hostinger-api-mcp 3.11.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +101 -22
  3. package/gemini-extension.json +1 -1
  4. package/package.json +1 -1
  5. package/server.json +2 -2
  6. package/skills/deploy-to-hosting/SKILL.md +1 -1
  7. package/src/core/runtime.js +117 -28
  8. package/src/core/runtime.ts +117 -28
  9. package/src/core/skills.js +3 -3
  10. package/src/core/skills.ts +3 -3
  11. package/src/core/tools/all.js +253 -11
  12. package/src/core/tools/all.ts +253 -11
  13. package/src/core/tools/hosting.js +23 -9
  14. package/src/core/tools/hosting.ts +23 -9
  15. package/src/core/tools/reach.js +230 -2
  16. package/src/core/tools/reach.ts +230 -2
  17. package/src/servers/agency-hosting.js +1 -1
  18. package/src/servers/agency-hosting.ts +1 -1
  19. package/src/servers/agent-readiness.js +1 -1
  20. package/src/servers/agent-readiness.ts +1 -1
  21. package/src/servers/all.js +1 -1
  22. package/src/servers/all.ts +1 -1
  23. package/src/servers/billing.js +1 -1
  24. package/src/servers/billing.ts +1 -1
  25. package/src/servers/dns.js +1 -1
  26. package/src/servers/dns.ts +1 -1
  27. package/src/servers/domains.js +1 -1
  28. package/src/servers/domains.ts +1 -1
  29. package/src/servers/ecommerce.js +1 -1
  30. package/src/servers/ecommerce.ts +1 -1
  31. package/src/servers/horizons.js +1 -1
  32. package/src/servers/horizons.ts +1 -1
  33. package/src/servers/hosting.js +1 -1
  34. package/src/servers/hosting.ts +1 -1
  35. package/src/servers/mail.js +1 -1
  36. package/src/servers/mail.ts +1 -1
  37. package/src/servers/reach.js +1 -1
  38. package/src/servers/reach.ts +1 -1
  39. package/src/servers/vps.js +1 -1
  40. package/src/servers/vps.ts +1 -1
  41. package/src/servers/wordpress.js +1 -1
  42. package/src/servers/wordpress.ts +1 -1
  43. package/types.d.ts +159 -11
@@ -97,7 +97,7 @@ interface Skill { uri: string; frontmatter: Record<string, unknown>; resources:
97
97
 
98
98
  /**
99
99
  * MCP Server for Hostinger API
100
- * Generated from OpenAPI spec version 2.3.0
100
+ * Generated from OpenAPI spec version 2.4.0
101
101
  */
102
102
  class MCPServer {
103
103
  private readonly name: string;
@@ -580,15 +580,29 @@ class MCPServer {
580
580
  return this.executeApiCall(tool, params);
581
581
  }
582
582
 
583
- // The website for a domain, matched exactly (the list endpoint filters by substring).
583
+ // The website for a domain, matched exactly. The list endpoint filters by substring and pages its
584
+ // results, so a short domain can match many sites: read pages until the exact one turns up or a page
585
+ // comes back short, up to a limit.
584
586
  private async findWebsite(domain: string): Promise<any> {
585
- const response = await this.callOperation('hosting_websites_list', { domain });
586
- const sites = response && Array.isArray(response.data) ? response.data : [];
587
- const site = sites.find((website: any) => website.domain && website.domain.toLowerCase() === domain.toLowerCase());
588
- if (!site) {
589
- throw new Error('No website found for domain: ' + domain);
587
+ const perPage = 100;
588
+ const maxPages = 10;
589
+ for (let page = 1; page <= maxPages; page++) {
590
+ const response = await this.callOperation('hosting_websites_list', { domain, page, per_page: perPage });
591
+ const sites = response && Array.isArray(response.data) ? response.data : [];
592
+ const site = sites.find((website: any) => website.domain && website.domain.toLowerCase() === domain.toLowerCase());
593
+ if (site) {
594
+ // Agency Plan (U4S) websites come back without a username, and every later call would fail with
595
+ // an unclear API error.
596
+ if (typeof site.username !== 'string') {
597
+ throw new Error(site.domain + ' is an Agency Plan website: Git deploys and build status here work only for Shared and Cloud hosting websites. Use the agency-hosting operations for it.');
598
+ }
599
+ return site;
600
+ }
601
+ if (sites.length < perPage) {
602
+ break;
603
+ }
590
604
  }
591
- return site;
605
+ throw new Error('No website found for domain: ' + domain);
592
606
  }
593
607
 
594
608
  private hosting_deploy_types = ['static', 'nodejs', 'wordpress-site', 'wordpress-plugin', 'wordpress-theme'];
@@ -797,9 +811,11 @@ class MCPServer {
797
811
  // directory, and the API stores them as null, so a missing one is sent as null. Detection supplies
798
812
  // the build script but only guesses a root-level entry file, so a compiled app (TypeScript) must
799
813
  // pass entryFile.
800
- // storedSettings tells base apart: settings stored for the site (Git deploy) belong to whatever
801
- // framework it ran before, so a different appType starts from a clean build step, while settings
802
- // detected from the project itself (archive deploy) are always right to keep.
814
+ // An explicit null for outputDirectory, buildScript or entryFile clears the stored value, so an app
815
+ // can drop a build step it no longer has.
816
+ // storedSettings tells base apart: settings stored for the site or detected from a Git branch (Git
817
+ // deploy) belong to the framework they were stored or detected for, so a different appType starts
818
+ // from a clean build step, while settings detected from the archive being deployed are kept.
803
819
  private hosting_deploy_nodeSettings(base: Record<string, any> | null, params: Record<string, any>, storedSettings: boolean): Record<string, any> {
804
820
  const given = base || {};
805
821
  const appType = params.appType ?? given.app_type;
@@ -808,6 +824,7 @@ class MCPServer {
808
824
  if (appType === undefined || appType === null) {
809
825
  throw new Error("Missing Node.js build setting: appType. Pass it to hosting_deploy, with outputDirectory and buildScript when the app has a build step; read them from the project's package.json.");
810
826
  }
827
+ const cleared = (name: string): boolean => name in params && params[name] === null;
811
828
  const optional: Record<string, any> = {
812
829
  entry_file: params.entryFile ?? stored.entry_file,
813
830
  package_manager: params.packageManager ?? stored.package_manager,
@@ -816,9 +833,10 @@ class MCPServer {
816
833
  node_version: params.nodeVersion ?? stored.node_version ?? 22,
817
834
  app_type: appType,
818
835
  root_directory: params.rootDirectory ?? stored.root_directory ?? '.',
819
- output_directory: params.outputDirectory ?? stored.output_directory ?? null,
820
- build_script: params.buildScript ?? stored.build_script ?? null,
836
+ output_directory: cleared('outputDirectory') ? null : params.outputDirectory ?? stored.output_directory ?? null,
837
+ build_script: cleared('buildScript') ? null : params.buildScript ?? stored.build_script ?? null,
821
838
  ...Object.fromEntries(Object.entries(optional).filter(([, value]) => value !== undefined && value !== null)),
839
+ ...(cleared('entryFile') ? { entry_file: null } : {}),
822
840
  };
823
841
  }
824
842
 
@@ -866,6 +884,9 @@ class MCPServer {
866
884
  // Read only now that git is known to be an object.
867
885
  if (git !== undefined) {
868
886
  this.hosting_deploy_gitDirectory(git);
887
+ if (git.installationUuid !== undefined && (typeof git.installationUuid !== 'string' || git.installationUuid === '')) {
888
+ throw new Error('git.installationUuid must be a non-empty string when provided');
889
+ }
869
890
  }
870
891
  }
871
892
 
@@ -1095,23 +1116,59 @@ class MCPServer {
1095
1116
  };
1096
1117
  }
1097
1118
 
1098
- // The Git installation that can reach owner/name (GitLab owners may contain slashes).
1099
- private async hosting_deploy_findRepository(fullName: string): Promise<{ installationUuid: string; owner: string; repository: string }> {
1119
+ // The Git installation that can reach owner/name (GitLab owners may contain slashes). Every active
1120
+ // installation is checked, not just until the first match: a GitHub and a GitLab mirror can share a
1121
+ // path, the installations come in no set order, and the same request must not deploy one repository
1122
+ // one time and the other the next, overwriting the site. With an installation given, the name is taken as it is: the repository list stops at 500 entries, so a
1123
+ // repository past that cannot be found by searching it.
1124
+ private async hosting_deploy_findRepository(fullName: string, installationUuid?: string): Promise<{ installationUuid: string; owner: string; repository: string }> {
1100
1125
  if (!fullName.includes('/')) {
1101
1126
  throw new Error('git.repository must be given as owner/name');
1102
1127
  }
1128
+ if (installationUuid !== undefined) {
1129
+ const slash = fullName.lastIndexOf('/');
1130
+ const owner = fullName.slice(0, slash);
1131
+ const repository = fullName.slice(slash + 1);
1132
+ if (owner === '' || repository === '') {
1133
+ throw new Error('git.repository must be given as owner/name');
1134
+ }
1135
+ return { installationUuid, owner, repository };
1136
+ }
1103
1137
  const installations = await this.callOperation('hosting_git_list-installations', { status: 'active' });
1104
1138
  if (!Array.isArray(installations) || installations.length === 0) {
1105
1139
  throw new Error('No active Git installation. Connect GitHub or GitLab once in hPanel (Websites, Manage, Advanced, Git), then try again.');
1106
1140
  }
1141
+ // The API returns at most this many repositories per installation. A full list without the
1142
+ // repository may still hold it past the cut, so a match elsewhere cannot be trusted to be the only one.
1143
+ const repositoryLimit = 500;
1144
+ const truncated: any[] = [];
1145
+ const matches: Array<{ installation: any; installationUuid: string; owner: string; repository: string }> = [];
1107
1146
  for (const installation of installations) {
1108
- const repositories = await this.callOperation('hosting_git_list-installation-repositories', { uuid: installation.uuid });
1109
- const repo = (Array.isArray(repositories) ? repositories : []).find((candidate: any) => String(candidate.full_name).toLowerCase() === fullName.toLowerCase());
1147
+ const response = await this.callOperation('hosting_git_list-installation-repositories', { uuid: installation.uuid });
1148
+ const repositories = Array.isArray(response) ? response : [];
1149
+ const repo = repositories.find((candidate: any) => String(candidate.full_name).toLowerCase() === fullName.toLowerCase());
1110
1150
  if (repo) {
1111
- return { installationUuid: installation.uuid, owner: repo.owner, repository: repo.name };
1151
+ matches.push({ installation, installationUuid: installation.uuid, owner: repo.owner, repository: repo.name });
1152
+ } else if (repositories.length >= repositoryLimit) {
1153
+ truncated.push(installation);
1154
+ }
1155
+ }
1156
+ const describe = (installation: any): string => (typeof installation.provider === 'string' ? installation.provider : 'git') + ' account ' + (typeof installation.account_login === 'string' ? installation.account_login : '?') + ' (installation ' + installation.uuid + ')';
1157
+ if (matches.length > 1) {
1158
+ throw new Error('Repository ' + fullName + ' is in ' + matches.length + ' of your Git installations: ' + matches.map((match) => describe(match.installation)).join('; ') + '. Pass git.installationUuid to say which one to deploy.');
1159
+ }
1160
+ if (matches.length === 1) {
1161
+ if (truncated.length > 0) {
1162
+ throw new Error('Repository ' + fullName + ' is in ' + describe(matches[0].installation) + ', but it may also be in ' + truncated.map(describe).join('; ') + ': repository lists stop at the first ' + repositoryLimit + '. Pass git.installationUuid to say which one to deploy.');
1112
1163
  }
1164
+ const { installationUuid, owner, repository } = matches[0];
1165
+ return { installationUuid, owner, repository };
1166
+ }
1167
+ const notFound = 'Repository ' + fullName + ' was not found in your ' + installations.length + ' active Git installation(s). Check the name, or give the Git provider access to it in hPanel (Websites, Manage, Advanced, Git).';
1168
+ if (truncated.length > 0) {
1169
+ throw new Error(notFound + ' An installation lists only its first ' + repositoryLimit + ' repositories: if it has more, pass git.installationUuid (from hosting_git_list-installations) to name the repository directly.');
1113
1170
  }
1114
- throw new Error('Repository ' + fullName + ' was not found in your ' + installations.length + ' active Git installation(s). Check the name, or give the Git provider access to it in hPanel (Websites, Manage, Advanced, Git).');
1171
+ throw new Error(notFound);
1115
1172
  }
1116
1173
 
1117
1174
  // What a Git deploy is. There are no local files to look at, so guessing is dangerous: a Node.js
@@ -1138,7 +1195,15 @@ class MCPServer {
1138
1195
  if (type === 'nodejs' && directory !== '') {
1139
1196
  throw new Error('git.directory is for static deploys: a Node.js build has no target folder');
1140
1197
  }
1141
- const repo = await this.hosting_deploy_findRepository(git.repository);
1198
+ // On these sites saving auto-deployment settings clones nothing, so a static deploy would report
1199
+ // success and publish nothing.
1200
+ if (type === 'static' && website.website_type === 'nodejs') {
1201
+ throw new Error('This website runs a Node.js app: deploy it with type nodejs, or leave type out. Saving Git settings on a Node.js website does not deploy anything.');
1202
+ }
1203
+ if (type === 'static' && website.website_type === 'builder') {
1204
+ throw new Error('This is a Website Builder website, which cannot deploy from Git: saving Git settings on it does not deploy anything.');
1205
+ }
1206
+ const repo = await this.hosting_deploy_findRepository(git.repository, git.installationUuid);
1142
1207
 
1143
1208
  if (type === 'static') {
1144
1209
  // Saving the settings deploys the branch now and again on every push.
@@ -1158,9 +1223,31 @@ class MCPServer {
1158
1223
  throw error;
1159
1224
  }
1160
1225
  }
1161
- const settings = this.hosting_deploy_nodeSettings(stored, params, true);
1162
- if (this.hosting_deploy_hasNodeOverrides(params)) {
1163
- // Later pushes build with the stored settings, not with this request: keep what was given.
1226
+ // The API can return all-null settings, which mean nothing is stored.
1227
+ const hasStored = stored !== null && typeof stored === 'object' && stored.app_type !== null && stored.app_type !== undefined;
1228
+ let settings: Record<string, any>;
1229
+ if (hasStored) {
1230
+ settings = this.hosting_deploy_nodeSettings(stored, params, true);
1231
+ } else {
1232
+ // A first deploy: detect the settings from the branch's package.json. The request's settings beat
1233
+ // them, and a different appType drops the build step detected for the other framework.
1234
+ let detected: any = null;
1235
+ try {
1236
+ detected = await this.callOperation('hosting_nodejs_build-settings-from-repository', {
1237
+ username, domain, owner: repo.owner, repository: repo.repository, branch: git.branch, installation_uuid: repo.installationUuid,
1238
+ });
1239
+ } catch (error) {
1240
+ if (!(error instanceof ApiExecutionError && error.status === 422)) {
1241
+ throw error;
1242
+ }
1243
+ if (params.appType === undefined || params.appType === null) {
1244
+ throw new Error('No Node.js build settings could be detected from the ' + git.branch + ' branch (' + this.formatToolError(error) + '). Pass appType, with outputDirectory and buildScript when the app has a build step, or deploy with type static if the branch has no Node.js app.');
1245
+ }
1246
+ }
1247
+ settings = this.hosting_deploy_nodeSettings(detected, params, true);
1248
+ }
1249
+ if (!hasStored || this.hosting_deploy_hasNodeOverrides(params)) {
1250
+ // Later pushes build with the stored settings, not with this request: keep what was given or detected.
1164
1251
  await this.callOperation('hosting_nodejs_update-build-settings', { username, domain, ...settings });
1165
1252
  }
1166
1253
  const build = await this.callOperation('hosting_nodejs_start-build', {
@@ -1178,10 +1265,11 @@ class MCPServer {
1178
1265
  // Local checks first, so a bad path fails before any request is made.
1179
1266
  const type = this.hosting_deploy_resolveType(params);
1180
1267
  const website = await this.findWebsite(domain);
1181
- return this.hosting_deploy_fromPath(website.username, domain, type, params);
1268
+ return this.hosting_deploy_fromPath(website.username, website.domain, type, params);
1182
1269
  }
1270
+ // The lookup ignores case: the API calls and the result use the website's own spelling.
1183
1271
  const website = await this.findWebsite(domain);
1184
- return this.hosting_deploy_fromGit(website, domain, git, params);
1272
+ return this.hosting_deploy_fromGit(website, website.domain, git, params);
1185
1273
  }
1186
1274
 
1187
1275
  // The failure analysis re-runs on every call and is limited to 5 a minute, so keep one request per
@@ -1227,9 +1315,9 @@ class MCPServer {
1227
1315
  }
1228
1316
 
1229
1317
  private async handleHostingGetDeployment(params: Record<string, any>): Promise<any> {
1230
- const { domain, buildUuid, fromLine } = params;
1318
+ const { domain: requestedDomain, buildUuid, fromLine } = params;
1231
1319
 
1232
- if (!domain || typeof domain !== 'string') {
1320
+ if (!requestedDomain || typeof requestedDomain !== 'string') {
1233
1321
  throw new Error('domain is required and must be a string');
1234
1322
  }
1235
1323
  if (buildUuid !== undefined && (typeof buildUuid !== 'string' || buildUuid === '')) {
@@ -1239,7 +1327,8 @@ class MCPServer {
1239
1327
  throw new Error('fromLine must be a non-negative integer when provided');
1240
1328
  }
1241
1329
 
1242
- const { username } = await this.findWebsite(domain);
1330
+ // The lookup ignores case: from here on use the website's own spelling, so one site has one analysis cache key.
1331
+ const { username, domain } = await this.findWebsite(requestedDomain);
1243
1332
  const target = { username, domain };
1244
1333
 
1245
1334
  const gitSettings = await this.callOperation('hosting_git_auto-deployment-settings', target);
@@ -41,10 +41,10 @@ export default [
41
41
  "resources": [
42
42
  {
43
43
  "uri": "skill://deploy-to-hosting/SKILL.md",
44
- "digest": "sha256:cfc166fd15a7956339aa607c7118d5b0199a222dfb473e99281a1850dc2da457",
45
- "size": 14031,
44
+ "digest": "sha256:276a0d8b11a6fb8fcf913a0e94f9a2650d551764a3eade27d7be0db3cb5a8912",
45
+ "size": 14033,
46
46
  "mimeType": "text/markdown",
47
- "text": "---\nname: deploy-to-hosting\ndescription: \"Deploy an existing project to a website on Hostinger web hosting (Shared, Cloud or Agency plans) and keep it deployed: picks the right deploy for static sites, Node.js apps (Next.js, Nuxt, Express, Vite and similar), PHP apps and WordPress plugins or themes, sets up auto-deploy from GitHub or GitLab, manages Node.js environment variables and MySQL databases, and verifies the live site. Triggers: deploy this project, deploy to Hostinger, push my app live, redeploy, set up auto-deploy, connect my GitHub repo, add environment variables, my app needs a database, deploy my WordPress plugin or theme.\"\n---\n\n# Deploy to hosting\n\nShip a project that already exists — on disk or in a Git repository — to a Hostinger website. Building a new site from a prompt (design, store, blog) is the `hostinger-headless` skill; this one takes code as it is.\n\n## Calling the operations\n\n- Read the `inputSchema` that `search` returns before the first call of each operation. If a name below is rejected as unknown, `search` for what the step does (e.g. \"deploy static\").\n- Batch reads with `multi-execute`; values pass between steps as `$steps.<i>.<path>`. Batches chain operations from one server only — the Hostinger Connector runs `hosting`, `wordpress` and `agency-hosting` as separate servers.\n- Every deploy overwrites the website's current contents. Confirm the target domain with the user before the first deploy to a site that already has content.\n\n## 1. Pick the target website\n\n- `.hostinger/site.json` in the project: reuse its `domain` and `username` (or `website_uid`).\n- Otherwise look the domain up with `hosting_websites_list` (Shared and Cloud; substring match — take the exact entry) and `agency-hosting_websites_list-plan` (Agency; keep `details.uid`).\n- No website yet:\n - Shared and Cloud: take a domain (the user's own, or `hosting_domains_generate-free-subdomain`) and an `order_id` from `hosting_orders_list`, then `hosting_websites_create`. `datacenter_code` is needed only for the first website on a new plan (first entry of `hosting_datacenters_list`). Poll `hosting_websites_list-setups` with `domain` every 10–15 s until `status: completed` — uploads and deploys return 404 or 409 before that.\n - Agency: `agency-hosting_website-setups_create` with `flavor: \"php-fpm\"` (plus `type: \"node-static\"` for built frontends), `settings.php.version` from `agency-hosting_php_list-versions-for-order` and `datacenter_code` from `agency-hosting_datacenters_list`. Poll `agency-hosting_website-setups_status` until `completed`; it returns the `website_uid`.\n- Node.js apps run on Business and Cloud plans (`hostinger_business_*`, `cloud_*` in `hosting_orders_list`). On other plans, build locally and deploy the output as a static site.\n\n## 2. Choose the deploy\n\n**Shared and Cloud, on the local `hostinger-api-mcp` server:** one operation does every deploy — `hosting_deploy` with `domain` and either `path` (a directory on this machine) or `git: { repository: \"owner/name\", branch }`. Given a directory it detects what the project is, packs it itself (it skips `node_modules`, `.git` and `.env` files, listing the `.env` ones as `skippedFiles` so you can set their values with `hosting_nodejs_replace-environment-variables`; it does not read `.gitignore`, so keep build output out of the directory) and does the upload, build and Git setup. Do not archive the directory or call upload and build operations by hand.\n\n| Project | `hosting_deploy` treats it as |\n| --- | --- |\n| Plain HTML/CSS/JS, or a framework's static build output | `static` — extracts as-is |\n| `package.json` with a build script, a start script or a server dependency (Next.js, Nuxt, Express, Vite…) | `nodejs` — starts a build, returns `buildUuid` |\n| PHP code, no build step | `static` |\n| WordPress plugin folder (a `Plugin Name:` header) or theme folder (`Theme Name:` in `style.css`) | `wordpress-plugin` / `wordpress-theme`; `activate: true` activates a theme |\n| A whole WordPress site from elsewhere | `wordpress-site`, with `databaseDump` — see the `migrate-to-hosting` skill |\n\n- Pass `type` to override detection. It is required for an archive (an archive cannot be inspected) and when a directory matches more than one WordPress type, which `hosting_deploy` rejects as ambiguous.\n- Node.js build settings come from the project; detection covers the framework and the build script. Set `appType`, `outputDirectory`, `buildScript`, `entryFile`, `nodeVersion` (default 22), `rootDirectory` or `packageManager` when it is wrong. A TypeScript or other compiled server app always needs `entryFile` set to the built file (for example `dist/server.js`): detection only guesses a root-level file, and the build fails with \"Entry file not found\". The entry file is relative to the root directory for express, fastify, hono, react-router and other, and to the output directory for nest, nuxt, nitro, astro and svelte-kit (`main.js`, not `dist/main.js`); Next.js ignores it. Per-framework values are in the Hostinger docs under Node.js (Supported Frameworks, Build Settings).\n- Static, PHP, plugin and theme deploys are live when the call returns. A WordPress site import only starts (`status: import_started`) and finishes extracting a few minutes later; do not report it complete or verify it before then. Node.js builds run on the server: see step 3.\n\n**Agency:** the website's type decides (`agency-hosting_websites_get`): sites created as `node-static` take `agency-hosting_deploy-node-static-website`, which runs the build when the project has one; every other site takes `agency-hosting_deploy-php-application`, which extracts the archive as-is. They deploy archives, synchronously: the site is live when the call returns. WordPress plugins and themes on Agency sites are installed from wp-admin.\n\nArchive rules for Agency (name archives `name_YYYYMMDD_HHMMSS.zip`) and for archives you build yourself:\n\n- **Static:** build locally first. `index.html` must be at the archive root, not inside a folder: `cd dist && zip -r ../site_20260101_120000.zip .`\n- **Node.js source:** no `node_modules/`, no build output (`dist/`, `.next/`, `build/`), no `.env*`, nothing matched by `.gitignore`; 50 MB at most. `git archive --format=zip -o app_20260101_120000.zip HEAD` produces exactly the committed files — mention that uncommitted changes are left out.\n\n`hosting_deploy` with `path` and the `agency-hosting_deploy-*` operations read files from this machine, so only the local server has them. The hosted server (`mcp.hostinger.com`) has `hosting_deploy` for Git repositories only (step 4) and `hosting_get-deployment` (step 3); to deploy files from this machine there, upload them yourself as below.\n\n### Without the local deploy operations (local files on the hosted server; Agency on either server)\n\n1. Get upload credentials: `hosting_files_generate-upload-url` (`username`, `domain`); Agency: `agency-hosting_files_generate-upload-url` (`website_uid`). Both return `url`, `auth_key` and `rest_auth_key`, which authenticate the upload instead of the API token.\n2. Upload each file with TUS, where `DEST` is its path in the website's storage:\n\n```bash\nSIZE=$(wc -c < \"$FILE\" | tr -d ' ')\ncurl -sS -X POST \"$URL/$DEST?override=true\" -H \"X-Auth: $AUTH_KEY\" -H \"X-Auth-Rest: $REST_AUTH_KEY\" -H \"Tus-Resumable: 1.0.0\" -H \"Upload-Length: $SIZE\" -H \"Upload-Offset: 0\"\ncurl -sS -X PATCH \"$URL/$DEST?override=true\" -H \"X-Auth: $AUTH_KEY\" -H \"X-Auth-Rest: $REST_AUTH_KEY\" -H \"Tus-Resumable: 1.0.0\" -H \"Content-Type: application/offset+octet-stream\" -H \"Upload-Offset: 0\" --data-binary \"@$FILE\"\n```\n\n The first call returns `201`, the second `204` with an `Upload-Offset` header equal to the file size. `override=true` makes a retry safe.\n3. Deploy from the uploaded files (`RANDOM8` is any fresh 8-character string, e.g. `$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c 8)`):\n\n| Project | `DEST` | Then |\n| --- | --- | --- |\n| Static or PHP | `site.zip` | `hosting_websites_deploy-static-site-archive` with `archive_path: \"site.zip\"` |\n| Node.js | `app.zip` | `hosting_nodejs_build-settings-from-archive` with `archive_path: \"app.zip\"`, then `hosting_nodejs_start-build` with those settings, `source_type: \"archive\"` and `source_options.archive_path: \"app.zip\"` |\n| WordPress plugin | every file, as `wp-content/plugins/SLUG-RANDOM8/<path in the plugin>` | `wordpress_plugins_deploy` with `slug` and `plugin_path: \"SLUG-RANDOM8\"` |\n| WordPress theme | every file, as `wp-content/themes/SLUG-RANDOM8/<path in the theme>` | `wordpress_themes_deploy` with `slug`, `theme_path: \"SLUG-RANDOM8\"` and optional `is_activated` |\n| Agency, extracted as-is | `.h5g/site.zip` | `agency-hosting_files_import-website-from-archive` with `archive_name: \"site.zip\"` |\n| Agency `node-static` | `.h5g/RANDOM8/app.zip` | `agency-hosting_websites_build-nodejs-assets` with `archive_path: \".h5g/RANDOM8\"` — the directory, not the file |\n\n## 3. Node.js builds\n\n`hosting_deploy` returns a `buildUuid` at once; the build runs on the server and takes minutes. Track it — and a build started with `hosting_nodejs_start-build` after an upload — with `hosting_get-deployment` (`domain`, optionally `buildUuid`; the latest build by default), polling every 10–20 s until `build.state` is `completed` or `failed`. It returns the build, its log and the stored build settings; pass the previous `logs.lines` as `fromLine` to read only new output.\n\n- Failed build: the response already carries `analysis` (fetched once per build; the API allows 5 a minute).\n- Wrong detection (framework, `output_directory`, or an entry file inside the build output, such as `dist/server.js` for a compiled app): deploy again with `appType`, `outputDirectory`, `buildScript` or `entryFile` set. An archive deploy sends its settings with the request and leaves the stored ones alone; a Git deploy also stores these values, so later pushes build with them.\n\nLocal files on the hosted server, deployed from an upload (step 2):\n\n- Wrong detection: run `hosting_nodejs_start-build` again with explicit values, `source_type: \"archive\"` and `source_options.archive_path` set to the archive's file name in the document root (check it is there with `hosting_files_list-website-and-directories`). Store the same values with `hosting_nodejs_update-build-settings`.\n- To review detected settings before any build: upload the archive with `hosting_files_generate-upload-url` (the TUS steps are in its description), then `hosting_nodejs_build-settings-from-archive`.\n\n## 4. Auto-deploy from Git\n\nOn both the local and the hosted server, `hosting_deploy` with `git: { repository: \"owner/name\", branch }` does all of it: it finds the Git installation that can reach the repository and deploys it as the `type` you give. Always pass `type: \"static\"` or `\"nodejs\"` unless the site already runs a Node.js app (or you pass Node.js settings such as `appType`): a new site is not detected as Node.js, and a Node.js repository deployed as static would publish its source. Then:\n- **PHP and static sites:** saves the auto-deployment settings, which deploys the branch immediately and again on every push;\n- **Node.js sites:** starts a build of the branch with the stored build settings; later pushes build the same way. A Git deploy has nothing to detect from, so when none are stored pass `appType` and `nodeVersion` (from `engines` in `package.json`), plus `outputDirectory` and `buildScript` if the app has a build step; they are stored for those pushes.\n\nWith no active installation it stops and says so: the user connects GitHub or GitLab once in hPanel under Websites → Manage → Advanced → Git; there is no API for that step. Confirm with `hosting_get-deployment`, which returns the stored `gitAutoDeployment`.\n\nAgency Plan sites have no Git operations; deploy archives.\n\n## 5. Environment variables (Node.js)\n\n`hosting_nodejs_replace-environment-variables` replaces the whole set — anything not sent is deleted.\n\n1. `hosting_nodejs_list-environment-variables` for the current keys. Values come back as `********`; never send those back.\n2. Build the full set: every current key plus the new ones, with real values from the project's `.env` or the user. When the real value of an existing key is unknown, ask — do not drop it.\n3. Send it once; the app restarts. Values baked in at build time (Next.js `NEXT_PUBLIC_*`, Vite `VITE_*`) need a new `hosting_nodejs_start-build`.\n\nKeys use uppercase letters, digits and underscores. Never commit `.env` files or repeat secret values back to the user.\n\n## 6. Database\n\n- **Node.js:** `hosting_databases_setup-website` creates a MySQL database and writes `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` and `DATABASE_URL` into the environment, then restarts the app. The password is generated and never returned. It fails with 422 when any of those keys exists — remove them first with the step 5 replace.\n- **PHP:** `hosting_databases_create` with `name`, `user`, a strong generated `password` and `website_domain`; read the prefixed full name back from `hosting_databases_list`. The app reads credentials from a config file on the server that stays out of Git, and connects to `localhost` or `127.0.0.1`.\n- **Agency:** `agency-hosting_databases_create-website`; the app connects to `localhost`.\n- The `srvNNNN.hstgr.io` host from `hosting_databases_list` is only for connections from outside Hostinger — never put it in the deployed app.\n\n## 7. Verify and record\n\n```bash\ncurl -s -o /dev/null -w \"%{http_code}\\n\" https://DOMAIN/\ncurl -s https://DOMAIN/ | grep -o \"TEXT THE PROJECT RENDERS\"\n```\n\nA `200` alone can be a placeholder page; match real copy. A new site may serve a default page briefly — clear it with `hosting_cache_clear-website` and retry before calling the deploy failed.\n\nWrite `.hostinger/site.json` in the project so later runs reuse the target: `{ \"domain\", \"username\" or \"website_uid\", \"type\": \"static\" | \"nodejs\" | \"php\" }`, keeping any fields already there.\n"
47
+ "text": "---\nname: deploy-to-hosting\ndescription: \"Deploy an existing project to a website on Hostinger web hosting (Shared, Cloud or Agency plans) and keep it deployed: picks the right deploy for static sites, Node.js apps (Next.js, Nuxt, Express, Vite and similar), PHP apps and WordPress plugins or themes, sets up auto-deploy from GitHub or GitLab, manages Node.js environment variables and MySQL databases, and verifies the live site. Triggers: deploy this project, deploy to Hostinger, push my app live, redeploy, set up auto-deploy, connect my GitHub repo, add environment variables, my app needs a database, deploy my WordPress plugin or theme.\"\n---\n\n# Deploy to hosting\n\nShip a project that already exists — on disk or in a Git repository — to a Hostinger website. Building a new site from a prompt (design, store, blog) is the `hostinger-headless` skill; this one takes code as it is.\n\n## Calling the operations\n\n- Read the `inputSchema` that `search` returns before the first call of each operation. If a name below is rejected as unknown, `search` for what the step does (e.g. \"deploy static\").\n- Batch reads with `multi-execute`; values pass between steps as `$steps.<i>.<path>`. Batches chain operations from one server only — the Hostinger Connector runs `hosting`, `wordpress` and `agency-hosting` as separate servers.\n- Every deploy overwrites the website's current contents. Confirm the target domain with the user before the first deploy to a site that already has content.\n\n## 1. Pick the target website\n\n- `.hostinger/site.json` in the project: reuse its `domain` and `username` (or `website_uid`).\n- Otherwise look the domain up with `hosting_websites_list` (Shared and Cloud; substring match — take the exact entry) and `agency-hosting_websites_list-plan` (Agency; keep `details.uid`).\n- No website yet:\n - Shared and Cloud: take a domain (the user's own, or `hosting_domains_generate-free-subdomain`) and an `order_id` from `hosting_orders_list`, then `hosting_websites_create`. `datacenter_code` is needed only for the first website on a new plan (first entry of `hosting_datacenters_list`). Poll `hosting_websites_list-setups` with `domain` every 10–15 s until `status: completed` — uploads and deploys return 404 or 409 before that.\n - Agency: `agency-hosting_website-setups_create` with `flavor: \"php-fpm\"` (plus `type: \"node-static\"` for built frontends), `settings.php.version` from `agency-hosting_php_list-versions-for-order` and `datacenter_code` from `agency-hosting_datacenters_list`. Poll `agency-hosting_website-setups_status` until `completed`; it returns the `website_uid`.\n- Node.js apps run on Business and Cloud plans (`hostinger_business_*`, `cloud_*` in `hosting_orders_list`). On other plans, build locally and deploy the output as a static site.\n\n## 2. Choose the deploy\n\n**Shared and Cloud, on the local `hostinger-api-mcp` server:** one operation does every deploy — `hosting_deploy` with `domain` and either `path` (a directory on this machine) or `git: { repository: \"owner/name\", branch }`. Given a directory it detects what the project is, packs it itself (it skips `node_modules`, `.git` and `.env` files, listing the `.env` ones as `skippedFiles` so you can set their values with `hosting_nodejs_replace-environment-variables`; it does not read `.gitignore`, so keep build output out of the directory) and does the upload, build and Git setup. Do not archive the directory or call upload and build operations by hand.\n\n| Project | `hosting_deploy` treats it as |\n| --- | --- |\n| Plain HTML/CSS/JS, or a framework's static build output | `static` — extracts as-is |\n| `package.json` with a build script, a start script or a server dependency (Next.js, Nuxt, Express, Vite…) | `nodejs` — starts a build, returns `buildUuid` |\n| PHP code, no build step | `static` |\n| WordPress plugin folder (a `Plugin Name:` header) or theme folder (`Theme Name:` in `style.css`) | `wordpress-plugin` / `wordpress-theme`; `activate: true` activates a theme |\n| A whole WordPress site from elsewhere | `wordpress-site`, with `databaseDump` — see the `migrate-to-hosting` skill |\n\n- Pass `type` to override detection. It is required for an archive (an archive cannot be inspected) and when a directory matches more than one WordPress type, which `hosting_deploy` rejects as ambiguous.\n- Node.js build settings come from the project; detection covers the framework and the build script. Set `appType`, `outputDirectory`, `buildScript`, `entryFile`, `nodeVersion` (default 22), `rootDirectory` or `packageManager` when it is wrong. A TypeScript or other compiled server app always needs `entryFile` set to the built file (for example `dist/server.js`): detection only guesses a root-level file, and the build fails with \"Entry file not found\". The entry file is relative to the root directory for express, fastify, hono, react-router and other, and to the output directory for nest, nuxt, nitro, astro and svelte-kit (`main.js`, not `dist/main.js`); Next.js ignores it. Per-framework values are in the Hostinger docs under Node.js (Supported Frameworks, Build Settings).\n- Static, PHP, plugin and theme deploys are live when the call returns. A WordPress site import only starts (`status: import_started`) and finishes extracting a few minutes later; do not report it complete or verify it before then. Node.js builds run on the server: see step 3.\n\n**Agency:** the website's type decides (`agency-hosting_websites_get`): sites created as `node-static` take `agency-hosting_deploy-node-static-website`, which runs the build when the project has one; every other site takes `agency-hosting_deploy-php-application`, which extracts the archive as-is. They deploy archives, synchronously: the site is live when the call returns. WordPress plugins and themes on Agency sites are installed from wp-admin.\n\nArchive rules for Agency (name archives `name_YYYYMMDD_HHMMSS.zip`) and for archives you build yourself:\n\n- **Static:** build locally first. `index.html` must be at the archive root, not inside a folder: `cd dist && zip -r ../site_20260101_120000.zip .`\n- **Node.js source:** no `node_modules/`, no build output (`dist/`, `.next/`, `build/`), no `.env*`, nothing matched by `.gitignore`; 50 MB at most. `git archive --format=zip -o app_20260101_120000.zip HEAD` produces exactly the committed files — mention that uncommitted changes are left out.\n\n`hosting_deploy` with `path` and the `agency-hosting_deploy-*` operations read files from this machine, so only the local server has them. The hosted server (`mcp.hostinger.com`) has `hosting_deploy` for Git repositories only (step 4) and `hosting_get-deployment` (step 3); to deploy files from this machine there, upload them yourself as below.\n\n### Without the local deploy operations (local files on the hosted server; Agency on either server)\n\n1. Get upload credentials: `hosting_files_generate-upload-url` (`username`, `domain`); Agency: `agency-hosting_files_generate-upload-url` (`website_uid`). Both return `url`, `auth_key` and `rest_auth_key`, which authenticate the upload instead of the API token.\n2. Upload each file with TUS, where `DEST` is its path in the website's storage:\n\n```bash\nSIZE=$(wc -c < \"$FILE\" | tr -d ' ')\ncurl -sS -X POST \"$URL/$DEST?override=true\" -H \"X-Auth: $AUTH_KEY\" -H \"X-Auth-Rest: $REST_AUTH_KEY\" -H \"Tus-Resumable: 1.0.0\" -H \"Upload-Length: $SIZE\" -H \"Upload-Offset: 0\"\ncurl -sS -X PATCH \"$URL/$DEST?override=true\" -H \"X-Auth: $AUTH_KEY\" -H \"X-Auth-Rest: $REST_AUTH_KEY\" -H \"Tus-Resumable: 1.0.0\" -H \"Content-Type: application/offset+octet-stream\" -H \"Upload-Offset: 0\" --data-binary \"@$FILE\"\n```\n\n The first call returns `201`, the second `204` with an `Upload-Offset` header equal to the file size. `override=true` makes a retry safe.\n3. Deploy from the uploaded files (`RANDOM8` is any fresh 8-character string, e.g. `$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c 8)`):\n\n| Project | `DEST` | Then |\n| --- | --- | --- |\n| Static or PHP | `site.zip` | `hosting_websites_deploy-static-site-archive` with `archive_path: \"site.zip\"` |\n| Node.js | `app.zip` | `hosting_nodejs_build-settings-from-archive` with `archive_path: \"app.zip\"`, then `hosting_nodejs_start-build` with those settings, `source_type: \"archive\"` and `source_options.archive_path: \"app.zip\"` |\n| WordPress plugin | every file, as `wp-content/plugins/SLUG-RANDOM8/<path in the plugin>` | `wordpress_plugins_deploy` with `slug` and `plugin_path: \"SLUG-RANDOM8\"` |\n| WordPress theme | every file, as `wp-content/themes/SLUG-RANDOM8/<path in the theme>` | `wordpress_themes_deploy` with `slug`, `theme_path: \"SLUG-RANDOM8\"` and optional `is_activated` |\n| Agency, extracted as-is | `.h5g/site.zip` | `agency-hosting_files_import-website-from-archive` with `archive_name: \"site.zip\"` |\n| Agency `node-static` | `.h5g/RANDOM8/app.zip` | `agency-hosting_websites_build-nodejs-assets` with `archive_path: \".h5g/RANDOM8\"` — the directory, not the file |\n\n## 3. Node.js builds\n\n`hosting_deploy` returns a `buildUuid` at once; the build runs on the server and takes minutes. Track it — and a build started with `hosting_nodejs_start-build` after an upload — with `hosting_get-deployment` (`domain`, optionally `buildUuid`; the latest build by default), polling every 10–20 s until `build.state` is `completed` or `failed`. It returns the build, its log and the stored build settings; pass the previous `logs.lines` as `fromLine` to read only new output.\n\n- Failed build: the response already carries `analysis` (fetched once per build; the API allows 5 a minute).\n- Wrong detection (framework, `output_directory`, or an entry file inside the build output, such as `dist/server.js` for a compiled app): deploy again with `appType`, `outputDirectory`, `buildScript` or `entryFile` set. An archive deploy sends its settings with the request and leaves the stored ones alone; a Git deploy also stores these values, so later pushes build with them.\n\nLocal files on the hosted server, deployed from an upload (step 2):\n\n- Wrong detection: run `hosting_nodejs_start-build` again with explicit values, `source_type: \"archive\"` and `source_options.archive_path` set to the archive's file name in the document root (check it is there with `hosting_files_list-website-and-directories`). Store the same values with `hosting_nodejs_update-build-settings`.\n- To review detected settings before any build: upload the archive with `hosting_files_generate-upload-url` (the TUS steps are in its description), then `hosting_nodejs_build-settings-from-archive`.\n\n## 4. Auto-deploy from Git\n\nOn both the local and the hosted server, `hosting_deploy` with `git: { repository: \"owner/name\", branch }` does all of it: it finds the Git installation that can reach the repository and deploys it as the `type` you give. Always pass `type: \"static\"` or `\"nodejs\"` unless the site already runs a Node.js app (or you pass Node.js settings such as `appType`): a new site is not detected as Node.js, and a Node.js repository deployed as static would publish its source. Then:\n- **PHP and static sites:** saves the auto-deployment settings, which deploys the branch immediately and again on every push;\n- **Node.js sites:** starts a build of the branch with the stored build settings; later pushes build the same way. When none are stored, they are detected from the branch's `package.json` and stored for those pushes; pass `appType`, `nodeVersion`, `outputDirectory`, `buildScript` or `entryFile` to override them (a compiled app still needs `entryFile`).\n\nWith no active installation it stops and says so: the user connects GitHub or GitLab once in hPanel under Websites → Manage → Advanced → Git; there is no API for that step. Confirm with `hosting_get-deployment`, which returns the stored `gitAutoDeployment`.\n\nAgency Plan sites have no Git operations; deploy archives.\n\n## 5. Environment variables (Node.js)\n\n`hosting_nodejs_replace-environment-variables` replaces the whole set — anything not sent is deleted.\n\n1. `hosting_nodejs_list-environment-variables` for the current keys. Values come back as `********`; never send those back.\n2. Build the full set: every current key plus the new ones, with real values from the project's `.env` or the user. When the real value of an existing key is unknown, ask — do not drop it.\n3. Send it once; the app restarts. Values baked in at build time (Next.js `NEXT_PUBLIC_*`, Vite `VITE_*`) need a new `hosting_nodejs_start-build`.\n\nKeys use uppercase letters, digits and underscores. Never commit `.env` files or repeat secret values back to the user.\n\n## 6. Database\n\n- **Node.js:** `hosting_databases_setup-website` creates a MySQL database and writes `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` and `DATABASE_URL` into the environment, then restarts the app. The password is generated and never returned. It fails with 422 when any of those keys exists — remove them first with the step 5 replace.\n- **PHP:** `hosting_databases_create` with `name`, `user`, a strong generated `password` and `website_domain`; read the prefixed full name back from `hosting_databases_list`. The app reads credentials from a config file on the server that stays out of Git, and connects to `localhost` or `127.0.0.1`.\n- **Agency:** `agency-hosting_databases_create-website`; the app connects to `localhost`.\n- The `srvNNNN.hstgr.io` host from `hosting_databases_list` is only for connections from outside Hostinger — never put it in the deployed app.\n\n## 7. Verify and record\n\n```bash\ncurl -s -o /dev/null -w \"%{http_code}\\n\" https://DOMAIN/\ncurl -s https://DOMAIN/ | grep -o \"TEXT THE PROJECT RENDERS\"\n```\n\nA `200` alone can be a placeholder page; match real copy. A new site may serve a default page briefly — clear it with `hosting_cache_clear-website` and retry before calling the deploy failed.\n\nWrite `.hostinger/site.json` in the project so later runs reuse the target: `{ \"domain\", \"username\" or \"website_uid\", \"type\": \"static\" | \"nodejs\" | \"php\" }`, keeping any fields already there.\n"
48
48
  }
49
49
  ]
50
50
  },
@@ -55,10 +55,10 @@ const skills: Skill[] = [
55
55
  "resources": [
56
56
  {
57
57
  "uri": "skill://deploy-to-hosting/SKILL.md",
58
- "digest": "sha256:cfc166fd15a7956339aa607c7118d5b0199a222dfb473e99281a1850dc2da457",
59
- "size": 14031,
58
+ "digest": "sha256:276a0d8b11a6fb8fcf913a0e94f9a2650d551764a3eade27d7be0db3cb5a8912",
59
+ "size": 14033,
60
60
  "mimeType": "text/markdown",
61
- "text": "---\nname: deploy-to-hosting\ndescription: \"Deploy an existing project to a website on Hostinger web hosting (Shared, Cloud or Agency plans) and keep it deployed: picks the right deploy for static sites, Node.js apps (Next.js, Nuxt, Express, Vite and similar), PHP apps and WordPress plugins or themes, sets up auto-deploy from GitHub or GitLab, manages Node.js environment variables and MySQL databases, and verifies the live site. Triggers: deploy this project, deploy to Hostinger, push my app live, redeploy, set up auto-deploy, connect my GitHub repo, add environment variables, my app needs a database, deploy my WordPress plugin or theme.\"\n---\n\n# Deploy to hosting\n\nShip a project that already exists — on disk or in a Git repository — to a Hostinger website. Building a new site from a prompt (design, store, blog) is the `hostinger-headless` skill; this one takes code as it is.\n\n## Calling the operations\n\n- Read the `inputSchema` that `search` returns before the first call of each operation. If a name below is rejected as unknown, `search` for what the step does (e.g. \"deploy static\").\n- Batch reads with `multi-execute`; values pass between steps as `$steps.<i>.<path>`. Batches chain operations from one server only — the Hostinger Connector runs `hosting`, `wordpress` and `agency-hosting` as separate servers.\n- Every deploy overwrites the website's current contents. Confirm the target domain with the user before the first deploy to a site that already has content.\n\n## 1. Pick the target website\n\n- `.hostinger/site.json` in the project: reuse its `domain` and `username` (or `website_uid`).\n- Otherwise look the domain up with `hosting_websites_list` (Shared and Cloud; substring match — take the exact entry) and `agency-hosting_websites_list-plan` (Agency; keep `details.uid`).\n- No website yet:\n - Shared and Cloud: take a domain (the user's own, or `hosting_domains_generate-free-subdomain`) and an `order_id` from `hosting_orders_list`, then `hosting_websites_create`. `datacenter_code` is needed only for the first website on a new plan (first entry of `hosting_datacenters_list`). Poll `hosting_websites_list-setups` with `domain` every 10–15 s until `status: completed` — uploads and deploys return 404 or 409 before that.\n - Agency: `agency-hosting_website-setups_create` with `flavor: \"php-fpm\"` (plus `type: \"node-static\"` for built frontends), `settings.php.version` from `agency-hosting_php_list-versions-for-order` and `datacenter_code` from `agency-hosting_datacenters_list`. Poll `agency-hosting_website-setups_status` until `completed`; it returns the `website_uid`.\n- Node.js apps run on Business and Cloud plans (`hostinger_business_*`, `cloud_*` in `hosting_orders_list`). On other plans, build locally and deploy the output as a static site.\n\n## 2. Choose the deploy\n\n**Shared and Cloud, on the local `hostinger-api-mcp` server:** one operation does every deploy — `hosting_deploy` with `domain` and either `path` (a directory on this machine) or `git: { repository: \"owner/name\", branch }`. Given a directory it detects what the project is, packs it itself (it skips `node_modules`, `.git` and `.env` files, listing the `.env` ones as `skippedFiles` so you can set their values with `hosting_nodejs_replace-environment-variables`; it does not read `.gitignore`, so keep build output out of the directory) and does the upload, build and Git setup. Do not archive the directory or call upload and build operations by hand.\n\n| Project | `hosting_deploy` treats it as |\n| --- | --- |\n| Plain HTML/CSS/JS, or a framework's static build output | `static` — extracts as-is |\n| `package.json` with a build script, a start script or a server dependency (Next.js, Nuxt, Express, Vite…) | `nodejs` — starts a build, returns `buildUuid` |\n| PHP code, no build step | `static` |\n| WordPress plugin folder (a `Plugin Name:` header) or theme folder (`Theme Name:` in `style.css`) | `wordpress-plugin` / `wordpress-theme`; `activate: true` activates a theme |\n| A whole WordPress site from elsewhere | `wordpress-site`, with `databaseDump` — see the `migrate-to-hosting` skill |\n\n- Pass `type` to override detection. It is required for an archive (an archive cannot be inspected) and when a directory matches more than one WordPress type, which `hosting_deploy` rejects as ambiguous.\n- Node.js build settings come from the project; detection covers the framework and the build script. Set `appType`, `outputDirectory`, `buildScript`, `entryFile`, `nodeVersion` (default 22), `rootDirectory` or `packageManager` when it is wrong. A TypeScript or other compiled server app always needs `entryFile` set to the built file (for example `dist/server.js`): detection only guesses a root-level file, and the build fails with \"Entry file not found\". The entry file is relative to the root directory for express, fastify, hono, react-router and other, and to the output directory for nest, nuxt, nitro, astro and svelte-kit (`main.js`, not `dist/main.js`); Next.js ignores it. Per-framework values are in the Hostinger docs under Node.js (Supported Frameworks, Build Settings).\n- Static, PHP, plugin and theme deploys are live when the call returns. A WordPress site import only starts (`status: import_started`) and finishes extracting a few minutes later; do not report it complete or verify it before then. Node.js builds run on the server: see step 3.\n\n**Agency:** the website's type decides (`agency-hosting_websites_get`): sites created as `node-static` take `agency-hosting_deploy-node-static-website`, which runs the build when the project has one; every other site takes `agency-hosting_deploy-php-application`, which extracts the archive as-is. They deploy archives, synchronously: the site is live when the call returns. WordPress plugins and themes on Agency sites are installed from wp-admin.\n\nArchive rules for Agency (name archives `name_YYYYMMDD_HHMMSS.zip`) and for archives you build yourself:\n\n- **Static:** build locally first. `index.html` must be at the archive root, not inside a folder: `cd dist && zip -r ../site_20260101_120000.zip .`\n- **Node.js source:** no `node_modules/`, no build output (`dist/`, `.next/`, `build/`), no `.env*`, nothing matched by `.gitignore`; 50 MB at most. `git archive --format=zip -o app_20260101_120000.zip HEAD` produces exactly the committed files — mention that uncommitted changes are left out.\n\n`hosting_deploy` with `path` and the `agency-hosting_deploy-*` operations read files from this machine, so only the local server has them. The hosted server (`mcp.hostinger.com`) has `hosting_deploy` for Git repositories only (step 4) and `hosting_get-deployment` (step 3); to deploy files from this machine there, upload them yourself as below.\n\n### Without the local deploy operations (local files on the hosted server; Agency on either server)\n\n1. Get upload credentials: `hosting_files_generate-upload-url` (`username`, `domain`); Agency: `agency-hosting_files_generate-upload-url` (`website_uid`). Both return `url`, `auth_key` and `rest_auth_key`, which authenticate the upload instead of the API token.\n2. Upload each file with TUS, where `DEST` is its path in the website's storage:\n\n```bash\nSIZE=$(wc -c < \"$FILE\" | tr -d ' ')\ncurl -sS -X POST \"$URL/$DEST?override=true\" -H \"X-Auth: $AUTH_KEY\" -H \"X-Auth-Rest: $REST_AUTH_KEY\" -H \"Tus-Resumable: 1.0.0\" -H \"Upload-Length: $SIZE\" -H \"Upload-Offset: 0\"\ncurl -sS -X PATCH \"$URL/$DEST?override=true\" -H \"X-Auth: $AUTH_KEY\" -H \"X-Auth-Rest: $REST_AUTH_KEY\" -H \"Tus-Resumable: 1.0.0\" -H \"Content-Type: application/offset+octet-stream\" -H \"Upload-Offset: 0\" --data-binary \"@$FILE\"\n```\n\n The first call returns `201`, the second `204` with an `Upload-Offset` header equal to the file size. `override=true` makes a retry safe.\n3. Deploy from the uploaded files (`RANDOM8` is any fresh 8-character string, e.g. `$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c 8)`):\n\n| Project | `DEST` | Then |\n| --- | --- | --- |\n| Static or PHP | `site.zip` | `hosting_websites_deploy-static-site-archive` with `archive_path: \"site.zip\"` |\n| Node.js | `app.zip` | `hosting_nodejs_build-settings-from-archive` with `archive_path: \"app.zip\"`, then `hosting_nodejs_start-build` with those settings, `source_type: \"archive\"` and `source_options.archive_path: \"app.zip\"` |\n| WordPress plugin | every file, as `wp-content/plugins/SLUG-RANDOM8/<path in the plugin>` | `wordpress_plugins_deploy` with `slug` and `plugin_path: \"SLUG-RANDOM8\"` |\n| WordPress theme | every file, as `wp-content/themes/SLUG-RANDOM8/<path in the theme>` | `wordpress_themes_deploy` with `slug`, `theme_path: \"SLUG-RANDOM8\"` and optional `is_activated` |\n| Agency, extracted as-is | `.h5g/site.zip` | `agency-hosting_files_import-website-from-archive` with `archive_name: \"site.zip\"` |\n| Agency `node-static` | `.h5g/RANDOM8/app.zip` | `agency-hosting_websites_build-nodejs-assets` with `archive_path: \".h5g/RANDOM8\"` — the directory, not the file |\n\n## 3. Node.js builds\n\n`hosting_deploy` returns a `buildUuid` at once; the build runs on the server and takes minutes. Track it — and a build started with `hosting_nodejs_start-build` after an upload — with `hosting_get-deployment` (`domain`, optionally `buildUuid`; the latest build by default), polling every 10–20 s until `build.state` is `completed` or `failed`. It returns the build, its log and the stored build settings; pass the previous `logs.lines` as `fromLine` to read only new output.\n\n- Failed build: the response already carries `analysis` (fetched once per build; the API allows 5 a minute).\n- Wrong detection (framework, `output_directory`, or an entry file inside the build output, such as `dist/server.js` for a compiled app): deploy again with `appType`, `outputDirectory`, `buildScript` or `entryFile` set. An archive deploy sends its settings with the request and leaves the stored ones alone; a Git deploy also stores these values, so later pushes build with them.\n\nLocal files on the hosted server, deployed from an upload (step 2):\n\n- Wrong detection: run `hosting_nodejs_start-build` again with explicit values, `source_type: \"archive\"` and `source_options.archive_path` set to the archive's file name in the document root (check it is there with `hosting_files_list-website-and-directories`). Store the same values with `hosting_nodejs_update-build-settings`.\n- To review detected settings before any build: upload the archive with `hosting_files_generate-upload-url` (the TUS steps are in its description), then `hosting_nodejs_build-settings-from-archive`.\n\n## 4. Auto-deploy from Git\n\nOn both the local and the hosted server, `hosting_deploy` with `git: { repository: \"owner/name\", branch }` does all of it: it finds the Git installation that can reach the repository and deploys it as the `type` you give. Always pass `type: \"static\"` or `\"nodejs\"` unless the site already runs a Node.js app (or you pass Node.js settings such as `appType`): a new site is not detected as Node.js, and a Node.js repository deployed as static would publish its source. Then:\n- **PHP and static sites:** saves the auto-deployment settings, which deploys the branch immediately and again on every push;\n- **Node.js sites:** starts a build of the branch with the stored build settings; later pushes build the same way. A Git deploy has nothing to detect from, so when none are stored pass `appType` and `nodeVersion` (from `engines` in `package.json`), plus `outputDirectory` and `buildScript` if the app has a build step; they are stored for those pushes.\n\nWith no active installation it stops and says so: the user connects GitHub or GitLab once in hPanel under Websites → Manage → Advanced → Git; there is no API for that step. Confirm with `hosting_get-deployment`, which returns the stored `gitAutoDeployment`.\n\nAgency Plan sites have no Git operations; deploy archives.\n\n## 5. Environment variables (Node.js)\n\n`hosting_nodejs_replace-environment-variables` replaces the whole set — anything not sent is deleted.\n\n1. `hosting_nodejs_list-environment-variables` for the current keys. Values come back as `********`; never send those back.\n2. Build the full set: every current key plus the new ones, with real values from the project's `.env` or the user. When the real value of an existing key is unknown, ask — do not drop it.\n3. Send it once; the app restarts. Values baked in at build time (Next.js `NEXT_PUBLIC_*`, Vite `VITE_*`) need a new `hosting_nodejs_start-build`.\n\nKeys use uppercase letters, digits and underscores. Never commit `.env` files or repeat secret values back to the user.\n\n## 6. Database\n\n- **Node.js:** `hosting_databases_setup-website` creates a MySQL database and writes `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` and `DATABASE_URL` into the environment, then restarts the app. The password is generated and never returned. It fails with 422 when any of those keys exists — remove them first with the step 5 replace.\n- **PHP:** `hosting_databases_create` with `name`, `user`, a strong generated `password` and `website_domain`; read the prefixed full name back from `hosting_databases_list`. The app reads credentials from a config file on the server that stays out of Git, and connects to `localhost` or `127.0.0.1`.\n- **Agency:** `agency-hosting_databases_create-website`; the app connects to `localhost`.\n- The `srvNNNN.hstgr.io` host from `hosting_databases_list` is only for connections from outside Hostinger — never put it in the deployed app.\n\n## 7. Verify and record\n\n```bash\ncurl -s -o /dev/null -w \"%{http_code}\\n\" https://DOMAIN/\ncurl -s https://DOMAIN/ | grep -o \"TEXT THE PROJECT RENDERS\"\n```\n\nA `200` alone can be a placeholder page; match real copy. A new site may serve a default page briefly — clear it with `hosting_cache_clear-website` and retry before calling the deploy failed.\n\nWrite `.hostinger/site.json` in the project so later runs reuse the target: `{ \"domain\", \"username\" or \"website_uid\", \"type\": \"static\" | \"nodejs\" | \"php\" }`, keeping any fields already there.\n"
61
+ "text": "---\nname: deploy-to-hosting\ndescription: \"Deploy an existing project to a website on Hostinger web hosting (Shared, Cloud or Agency plans) and keep it deployed: picks the right deploy for static sites, Node.js apps (Next.js, Nuxt, Express, Vite and similar), PHP apps and WordPress plugins or themes, sets up auto-deploy from GitHub or GitLab, manages Node.js environment variables and MySQL databases, and verifies the live site. Triggers: deploy this project, deploy to Hostinger, push my app live, redeploy, set up auto-deploy, connect my GitHub repo, add environment variables, my app needs a database, deploy my WordPress plugin or theme.\"\n---\n\n# Deploy to hosting\n\nShip a project that already exists — on disk or in a Git repository — to a Hostinger website. Building a new site from a prompt (design, store, blog) is the `hostinger-headless` skill; this one takes code as it is.\n\n## Calling the operations\n\n- Read the `inputSchema` that `search` returns before the first call of each operation. If a name below is rejected as unknown, `search` for what the step does (e.g. \"deploy static\").\n- Batch reads with `multi-execute`; values pass between steps as `$steps.<i>.<path>`. Batches chain operations from one server only — the Hostinger Connector runs `hosting`, `wordpress` and `agency-hosting` as separate servers.\n- Every deploy overwrites the website's current contents. Confirm the target domain with the user before the first deploy to a site that already has content.\n\n## 1. Pick the target website\n\n- `.hostinger/site.json` in the project: reuse its `domain` and `username` (or `website_uid`).\n- Otherwise look the domain up with `hosting_websites_list` (Shared and Cloud; substring match — take the exact entry) and `agency-hosting_websites_list-plan` (Agency; keep `details.uid`).\n- No website yet:\n - Shared and Cloud: take a domain (the user's own, or `hosting_domains_generate-free-subdomain`) and an `order_id` from `hosting_orders_list`, then `hosting_websites_create`. `datacenter_code` is needed only for the first website on a new plan (first entry of `hosting_datacenters_list`). Poll `hosting_websites_list-setups` with `domain` every 10–15 s until `status: completed` — uploads and deploys return 404 or 409 before that.\n - Agency: `agency-hosting_website-setups_create` with `flavor: \"php-fpm\"` (plus `type: \"node-static\"` for built frontends), `settings.php.version` from `agency-hosting_php_list-versions-for-order` and `datacenter_code` from `agency-hosting_datacenters_list`. Poll `agency-hosting_website-setups_status` until `completed`; it returns the `website_uid`.\n- Node.js apps run on Business and Cloud plans (`hostinger_business_*`, `cloud_*` in `hosting_orders_list`). On other plans, build locally and deploy the output as a static site.\n\n## 2. Choose the deploy\n\n**Shared and Cloud, on the local `hostinger-api-mcp` server:** one operation does every deploy — `hosting_deploy` with `domain` and either `path` (a directory on this machine) or `git: { repository: \"owner/name\", branch }`. Given a directory it detects what the project is, packs it itself (it skips `node_modules`, `.git` and `.env` files, listing the `.env` ones as `skippedFiles` so you can set their values with `hosting_nodejs_replace-environment-variables`; it does not read `.gitignore`, so keep build output out of the directory) and does the upload, build and Git setup. Do not archive the directory or call upload and build operations by hand.\n\n| Project | `hosting_deploy` treats it as |\n| --- | --- |\n| Plain HTML/CSS/JS, or a framework's static build output | `static` — extracts as-is |\n| `package.json` with a build script, a start script or a server dependency (Next.js, Nuxt, Express, Vite…) | `nodejs` — starts a build, returns `buildUuid` |\n| PHP code, no build step | `static` |\n| WordPress plugin folder (a `Plugin Name:` header) or theme folder (`Theme Name:` in `style.css`) | `wordpress-plugin` / `wordpress-theme`; `activate: true` activates a theme |\n| A whole WordPress site from elsewhere | `wordpress-site`, with `databaseDump` — see the `migrate-to-hosting` skill |\n\n- Pass `type` to override detection. It is required for an archive (an archive cannot be inspected) and when a directory matches more than one WordPress type, which `hosting_deploy` rejects as ambiguous.\n- Node.js build settings come from the project; detection covers the framework and the build script. Set `appType`, `outputDirectory`, `buildScript`, `entryFile`, `nodeVersion` (default 22), `rootDirectory` or `packageManager` when it is wrong. A TypeScript or other compiled server app always needs `entryFile` set to the built file (for example `dist/server.js`): detection only guesses a root-level file, and the build fails with \"Entry file not found\". The entry file is relative to the root directory for express, fastify, hono, react-router and other, and to the output directory for nest, nuxt, nitro, astro and svelte-kit (`main.js`, not `dist/main.js`); Next.js ignores it. Per-framework values are in the Hostinger docs under Node.js (Supported Frameworks, Build Settings).\n- Static, PHP, plugin and theme deploys are live when the call returns. A WordPress site import only starts (`status: import_started`) and finishes extracting a few minutes later; do not report it complete or verify it before then. Node.js builds run on the server: see step 3.\n\n**Agency:** the website's type decides (`agency-hosting_websites_get`): sites created as `node-static` take `agency-hosting_deploy-node-static-website`, which runs the build when the project has one; every other site takes `agency-hosting_deploy-php-application`, which extracts the archive as-is. They deploy archives, synchronously: the site is live when the call returns. WordPress plugins and themes on Agency sites are installed from wp-admin.\n\nArchive rules for Agency (name archives `name_YYYYMMDD_HHMMSS.zip`) and for archives you build yourself:\n\n- **Static:** build locally first. `index.html` must be at the archive root, not inside a folder: `cd dist && zip -r ../site_20260101_120000.zip .`\n- **Node.js source:** no `node_modules/`, no build output (`dist/`, `.next/`, `build/`), no `.env*`, nothing matched by `.gitignore`; 50 MB at most. `git archive --format=zip -o app_20260101_120000.zip HEAD` produces exactly the committed files — mention that uncommitted changes are left out.\n\n`hosting_deploy` with `path` and the `agency-hosting_deploy-*` operations read files from this machine, so only the local server has them. The hosted server (`mcp.hostinger.com`) has `hosting_deploy` for Git repositories only (step 4) and `hosting_get-deployment` (step 3); to deploy files from this machine there, upload them yourself as below.\n\n### Without the local deploy operations (local files on the hosted server; Agency on either server)\n\n1. Get upload credentials: `hosting_files_generate-upload-url` (`username`, `domain`); Agency: `agency-hosting_files_generate-upload-url` (`website_uid`). Both return `url`, `auth_key` and `rest_auth_key`, which authenticate the upload instead of the API token.\n2. Upload each file with TUS, where `DEST` is its path in the website's storage:\n\n```bash\nSIZE=$(wc -c < \"$FILE\" | tr -d ' ')\ncurl -sS -X POST \"$URL/$DEST?override=true\" -H \"X-Auth: $AUTH_KEY\" -H \"X-Auth-Rest: $REST_AUTH_KEY\" -H \"Tus-Resumable: 1.0.0\" -H \"Upload-Length: $SIZE\" -H \"Upload-Offset: 0\"\ncurl -sS -X PATCH \"$URL/$DEST?override=true\" -H \"X-Auth: $AUTH_KEY\" -H \"X-Auth-Rest: $REST_AUTH_KEY\" -H \"Tus-Resumable: 1.0.0\" -H \"Content-Type: application/offset+octet-stream\" -H \"Upload-Offset: 0\" --data-binary \"@$FILE\"\n```\n\n The first call returns `201`, the second `204` with an `Upload-Offset` header equal to the file size. `override=true` makes a retry safe.\n3. Deploy from the uploaded files (`RANDOM8` is any fresh 8-character string, e.g. `$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | head -c 8)`):\n\n| Project | `DEST` | Then |\n| --- | --- | --- |\n| Static or PHP | `site.zip` | `hosting_websites_deploy-static-site-archive` with `archive_path: \"site.zip\"` |\n| Node.js | `app.zip` | `hosting_nodejs_build-settings-from-archive` with `archive_path: \"app.zip\"`, then `hosting_nodejs_start-build` with those settings, `source_type: \"archive\"` and `source_options.archive_path: \"app.zip\"` |\n| WordPress plugin | every file, as `wp-content/plugins/SLUG-RANDOM8/<path in the plugin>` | `wordpress_plugins_deploy` with `slug` and `plugin_path: \"SLUG-RANDOM8\"` |\n| WordPress theme | every file, as `wp-content/themes/SLUG-RANDOM8/<path in the theme>` | `wordpress_themes_deploy` with `slug`, `theme_path: \"SLUG-RANDOM8\"` and optional `is_activated` |\n| Agency, extracted as-is | `.h5g/site.zip` | `agency-hosting_files_import-website-from-archive` with `archive_name: \"site.zip\"` |\n| Agency `node-static` | `.h5g/RANDOM8/app.zip` | `agency-hosting_websites_build-nodejs-assets` with `archive_path: \".h5g/RANDOM8\"` — the directory, not the file |\n\n## 3. Node.js builds\n\n`hosting_deploy` returns a `buildUuid` at once; the build runs on the server and takes minutes. Track it — and a build started with `hosting_nodejs_start-build` after an upload — with `hosting_get-deployment` (`domain`, optionally `buildUuid`; the latest build by default), polling every 10–20 s until `build.state` is `completed` or `failed`. It returns the build, its log and the stored build settings; pass the previous `logs.lines` as `fromLine` to read only new output.\n\n- Failed build: the response already carries `analysis` (fetched once per build; the API allows 5 a minute).\n- Wrong detection (framework, `output_directory`, or an entry file inside the build output, such as `dist/server.js` for a compiled app): deploy again with `appType`, `outputDirectory`, `buildScript` or `entryFile` set. An archive deploy sends its settings with the request and leaves the stored ones alone; a Git deploy also stores these values, so later pushes build with them.\n\nLocal files on the hosted server, deployed from an upload (step 2):\n\n- Wrong detection: run `hosting_nodejs_start-build` again with explicit values, `source_type: \"archive\"` and `source_options.archive_path` set to the archive's file name in the document root (check it is there with `hosting_files_list-website-and-directories`). Store the same values with `hosting_nodejs_update-build-settings`.\n- To review detected settings before any build: upload the archive with `hosting_files_generate-upload-url` (the TUS steps are in its description), then `hosting_nodejs_build-settings-from-archive`.\n\n## 4. Auto-deploy from Git\n\nOn both the local and the hosted server, `hosting_deploy` with `git: { repository: \"owner/name\", branch }` does all of it: it finds the Git installation that can reach the repository and deploys it as the `type` you give. Always pass `type: \"static\"` or `\"nodejs\"` unless the site already runs a Node.js app (or you pass Node.js settings such as `appType`): a new site is not detected as Node.js, and a Node.js repository deployed as static would publish its source. Then:\n- **PHP and static sites:** saves the auto-deployment settings, which deploys the branch immediately and again on every push;\n- **Node.js sites:** starts a build of the branch with the stored build settings; later pushes build the same way. When none are stored, they are detected from the branch's `package.json` and stored for those pushes; pass `appType`, `nodeVersion`, `outputDirectory`, `buildScript` or `entryFile` to override them (a compiled app still needs `entryFile`).\n\nWith no active installation it stops and says so: the user connects GitHub or GitLab once in hPanel under Websites → Manage → Advanced → Git; there is no API for that step. Confirm with `hosting_get-deployment`, which returns the stored `gitAutoDeployment`.\n\nAgency Plan sites have no Git operations; deploy archives.\n\n## 5. Environment variables (Node.js)\n\n`hosting_nodejs_replace-environment-variables` replaces the whole set — anything not sent is deleted.\n\n1. `hosting_nodejs_list-environment-variables` for the current keys. Values come back as `********`; never send those back.\n2. Build the full set: every current key plus the new ones, with real values from the project's `.env` or the user. When the real value of an existing key is unknown, ask — do not drop it.\n3. Send it once; the app restarts. Values baked in at build time (Next.js `NEXT_PUBLIC_*`, Vite `VITE_*`) need a new `hosting_nodejs_start-build`.\n\nKeys use uppercase letters, digits and underscores. Never commit `.env` files or repeat secret values back to the user.\n\n## 6. Database\n\n- **Node.js:** `hosting_databases_setup-website` creates a MySQL database and writes `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` and `DATABASE_URL` into the environment, then restarts the app. The password is generated and never returned. It fails with 422 when any of those keys exists — remove them first with the step 5 replace.\n- **PHP:** `hosting_databases_create` with `name`, `user`, a strong generated `password` and `website_domain`; read the prefixed full name back from `hosting_databases_list`. The app reads credentials from a config file on the server that stays out of Git, and connects to `localhost` or `127.0.0.1`.\n- **Agency:** `agency-hosting_databases_create-website`; the app connects to `localhost`.\n- The `srvNNNN.hstgr.io` host from `hosting_databases_list` is only for connections from outside Hostinger — never put it in the deployed app.\n\n## 7. Verify and record\n\n```bash\ncurl -s -o /dev/null -w \"%{http_code}\\n\" https://DOMAIN/\ncurl -s https://DOMAIN/ | grep -o \"TEXT THE PROJECT RENDERS\"\n```\n\nA `200` alone can be a placeholder page; match real copy. A new site may serve a default page briefly — clear it with `hosting_cache_clear-website` and retry before calling the deploy failed.\n\nWrite `.hostinger/site.json` in the project so later runs reuse the target: `{ \"domain\", \"username\" or \"website_uid\", \"type\": \"static\" | \"nodejs\" | \"php\" }`, keeping any fields already there.\n"
62
62
  }
63
63
  ]
64
64
  },