hostinger-api-mcp 3.11.0 → 3.11.1
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 +15 -0
- package/README.md +3 -18
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/deploy-to-hosting/SKILL.md +1 -1
- package/src/core/runtime.js +116 -27
- package/src/core/runtime.ts +116 -27
- package/src/core/skills.js +3 -3
- package/src/core/skills.ts +3 -3
- package/src/core/tools/all.js +23 -9
- package/src/core/tools/all.ts +23 -9
- package/src/core/tools/hosting.js +23 -9
- package/src/core/tools/hosting.ts +23 -9
- 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/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/types.d.ts +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ 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.11.1 — 2026-10-27
|
|
8
|
+
|
|
9
|
+
### Operations
|
|
10
|
+
|
|
11
|
+
- Hid 1 operation from search (it still runs through `execute`): `hosting_nodejs_build-settings-from-repository`
|
|
12
|
+
- Changed the description and input schema of `hosting_deploy`
|
|
13
|
+
|
|
14
|
+
### Also in this release
|
|
15
|
+
|
|
16
|
+
- Documentation: `README.md`
|
|
17
|
+
- Bundled agent skills: `skills/deploy-to-hosting/SKILL.md`
|
|
18
|
+
- Request handling and authentication: `src/core/runtime.js`, `src/core/runtime.ts`
|
|
19
|
+
- Other files: `src/core/skills.js`, `src/core/skills.ts`
|
|
20
|
+
- Build and packaging: `types.d.ts`
|
|
21
|
+
|
|
7
22
|
## v3.11.0 — 2026-10-27
|
|
8
23
|
|
|
9
24
|
### Also in this release
|
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ pnpm update -g @hostinger/mcp
|
|
|
69
69
|
|
|
70
70
|
This package installs the following MCP server commands:
|
|
71
71
|
|
|
72
|
-
- `hostinger-api-mcp` — unified server over every operation (
|
|
72
|
+
- `hostinger-api-mcp` — unified server over every operation (409 total)
|
|
73
73
|
- `hostinger-agency-hosting-mcp` — 42 operations for agency-hosting
|
|
74
74
|
- `hostinger-agent-readiness-mcp` — 3 operations for agent-readiness
|
|
75
75
|
- `hostinger-billing-mcp` — 9 operations for billing
|
|
@@ -77,7 +77,7 @@ This package installs the following MCP server commands:
|
|
|
77
77
|
- `hostinger-domains-mcp` — 42 operations for domains
|
|
78
78
|
- `hostinger-ecommerce-mcp` — 29 operations for ecommerce
|
|
79
79
|
- `hostinger-horizons-mcp` — 6 operations for horizons
|
|
80
|
-
- `hostinger-hosting-mcp` —
|
|
80
|
+
- `hostinger-hosting-mcp` — 66 operations for hosting
|
|
81
81
|
- `hostinger-mail-mcp` — 38 operations for mail
|
|
82
82
|
- `hostinger-reach-mcp` — 62 operations for reach
|
|
83
83
|
- `hostinger-vps-mcp` — 67 operations for vps
|
|
@@ -1816,7 +1816,7 @@ interface in the provided website URL.
|
|
|
1816
1816
|
|
|
1817
1817
|
#### hosting_deploy
|
|
1818
1818
|
|
|
1819
|
-
Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed. A plugin or theme goes into the site's WordPress installation, also when WordPress is in a folder of the document root (headless). Static, PHP, plugin and theme deploys are done when this returns; a WordPress site import only starts, and its files are extracted a few minutes later. From Git, pass type (static or nodejs) unless the site already runs a Node.js app or you pass Node.js settings; a static or PHP site deploys the branch now and again on every push; a Node.js site builds the branch now and on every later push, with the settings stored for the site (when none are stored,
|
|
1819
|
+
Deploy, publish or upload a website to Hostinger web hosting (Shared, Cloud): static HTML, PHP, a nodejs (Node.js) app, a whole WordPress site, or a WordPress plugin or theme, from a local directory, an archive or a GitHub or GitLab repository. It detects what the project is and does the upload, build and Git setup itself: do not call upload or build operations, and do not archive the directory yourself. WARNING: it overwrites the website's contents and cannot be undone; confirm the domain with the user before deploying to a site that already has content. A Node.js build runs on the server: this returns a buildUuid at once, then call hosting_get-deployment until the build state is completed or failed. A plugin or theme goes into the site's WordPress installation, also when WordPress is in a folder of the document root (headless). Static, PHP, plugin and theme deploys are done when this returns; a WordPress site import only starts, and its files are extracted a few minutes later. From Git, pass type (static or nodejs) unless the site already runs a Node.js app or you pass Node.js settings; a static or PHP site deploys the branch now and again on every push; a Node.js site builds the branch now and on every later push, with the settings stored for the site (when none are stored, they are detected from the branch's package.json; pass appType, nodeVersion, outputDirectory, buildScript or entryFile to override them). Git needs GitHub or GitLab connected once in hPanel.
|
|
1820
1820
|
|
|
1821
1821
|
- **Method**: `custom`
|
|
1822
1822
|
- **Path**: `custom`
|
|
@@ -2248,21 +2248,6 @@ to it.
|
|
|
2248
2248
|
- **Method**: `POST`
|
|
2249
2249
|
- **Path**: `/api/hosting/v1/headless/agent-reports`
|
|
2250
2250
|
|
|
2251
|
-
#### hosting_nodejs_build-settings-from-repository
|
|
2252
|
-
|
|
2253
|
-
Determine the build settings for a Node.js application from the package.json file of a branch
|
|
2254
|
-
in a Git repository. Use before starting a Git-source build to find out the auto-detected
|
|
2255
|
-
settings instead of guessing them.
|
|
2256
|
-
|
|
2257
|
-
The repository is read through a Git installation: take `installation_uuid` from
|
|
2258
|
-
`List Git installations` and `owner` and `repository` from `List Git installation repositories`.
|
|
2259
|
-
|
|
2260
|
-
Returns 422 when the installation is not found, the repository or branch is not found, or
|
|
2261
|
-
the branch has no package.json.
|
|
2262
|
-
|
|
2263
|
-
- **Method**: `GET`
|
|
2264
|
-
- **Path**: `/api/hosting/v1/accounts/{username}/websites/{domain}/nodejs/builds/settings/from-repository`
|
|
2265
|
-
|
|
2266
2251
|
#### hosting_nodejs_list-environment-variables
|
|
2267
2252
|
|
|
2268
2253
|
Lists the Node.js environment variables currently set for the website. Values are always
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/hostinger/api-mcp-server",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "3.11.
|
|
9
|
+
"version": "3.11.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@hostinger/mcp",
|
|
14
|
-
"version": "3.11.
|
|
14
|
+
"version": "3.11.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
|
@@ -86,7 +86,7 @@ Local files on the hosted server, deployed from an upload (step 2):
|
|
|
86
86
|
|
|
87
87
|
On 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:
|
|
88
88
|
- **PHP and static sites:** saves the auto-deployment settings, which deploys the branch immediately and again on every push;
|
|
89
|
-
- **Node.js sites:** starts a build of the branch with the stored build settings; later pushes build the same way.
|
|
89
|
+
- **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`).
|
|
90
90
|
|
|
91
91
|
With 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`.
|
|
92
92
|
|
package/src/core/runtime.js
CHANGED
|
@@ -543,15 +543,29 @@ class MCPServer {
|
|
|
543
543
|
return this.executeApiCall(tool, params);
|
|
544
544
|
}
|
|
545
545
|
|
|
546
|
-
// The website for a domain, matched exactly
|
|
546
|
+
// The website for a domain, matched exactly. The list endpoint filters by substring and pages its
|
|
547
|
+
// results, so a short domain can match many sites: read pages until the exact one turns up or a page
|
|
548
|
+
// comes back short, up to a limit.
|
|
547
549
|
async findWebsite(domain) {
|
|
548
|
-
const
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
550
|
+
const perPage = 100;
|
|
551
|
+
const maxPages = 10;
|
|
552
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
553
|
+
const response = await this.callOperation('hosting_websites_list', { domain, page, per_page: perPage });
|
|
554
|
+
const sites = response && Array.isArray(response.data) ? response.data : [];
|
|
555
|
+
const site = sites.find((website) => website.domain && website.domain.toLowerCase() === domain.toLowerCase());
|
|
556
|
+
if (site) {
|
|
557
|
+
// Agency Plan (U4S) websites come back without a username, and every later call would fail with
|
|
558
|
+
// an unclear API error.
|
|
559
|
+
if (typeof site.username !== 'string') {
|
|
560
|
+
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.');
|
|
561
|
+
}
|
|
562
|
+
return site;
|
|
563
|
+
}
|
|
564
|
+
if (sites.length < perPage) {
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
553
567
|
}
|
|
554
|
-
|
|
568
|
+
throw new Error('No website found for domain: ' + domain);
|
|
555
569
|
}
|
|
556
570
|
|
|
557
571
|
hosting_deploy_types = ['static', 'nodejs', 'wordpress-site', 'wordpress-plugin', 'wordpress-theme'];
|
|
@@ -760,9 +774,11 @@ class MCPServer {
|
|
|
760
774
|
// directory, and the API stores them as null, so a missing one is sent as null. Detection supplies
|
|
761
775
|
// the build script but only guesses a root-level entry file, so a compiled app (TypeScript) must
|
|
762
776
|
// pass entryFile.
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
//
|
|
777
|
+
// An explicit null for outputDirectory, buildScript or entryFile clears the stored value, so an app
|
|
778
|
+
// can drop a build step it no longer has.
|
|
779
|
+
// storedSettings tells base apart: settings stored for the site or detected from a Git branch (Git
|
|
780
|
+
// deploy) belong to the framework they were stored or detected for, so a different appType starts
|
|
781
|
+
// from a clean build step, while settings detected from the archive being deployed are kept.
|
|
766
782
|
hosting_deploy_nodeSettings(base, params, storedSettings) {
|
|
767
783
|
const given = base || {};
|
|
768
784
|
const appType = params.appType ?? given.app_type;
|
|
@@ -771,6 +787,7 @@ class MCPServer {
|
|
|
771
787
|
if (appType === undefined || appType === null) {
|
|
772
788
|
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.");
|
|
773
789
|
}
|
|
790
|
+
const cleared = (name) => name in params && params[name] === null;
|
|
774
791
|
const optional = {
|
|
775
792
|
entry_file: params.entryFile ?? stored.entry_file,
|
|
776
793
|
package_manager: params.packageManager ?? stored.package_manager,
|
|
@@ -779,9 +796,10 @@ class MCPServer {
|
|
|
779
796
|
node_version: params.nodeVersion ?? stored.node_version ?? 22,
|
|
780
797
|
app_type: appType,
|
|
781
798
|
root_directory: params.rootDirectory ?? stored.root_directory ?? '.',
|
|
782
|
-
output_directory: params.outputDirectory ?? stored.output_directory ?? null,
|
|
783
|
-
build_script: params.buildScript ?? stored.build_script ?? null,
|
|
799
|
+
output_directory: cleared('outputDirectory') ? null : params.outputDirectory ?? stored.output_directory ?? null,
|
|
800
|
+
build_script: cleared('buildScript') ? null : params.buildScript ?? stored.build_script ?? null,
|
|
784
801
|
...Object.fromEntries(Object.entries(optional).filter(([, value]) => value !== undefined && value !== null)),
|
|
802
|
+
...(cleared('entryFile') ? { entry_file: null } : {}),
|
|
785
803
|
};
|
|
786
804
|
}
|
|
787
805
|
|
|
@@ -829,6 +847,9 @@ class MCPServer {
|
|
|
829
847
|
// Read only now that git is known to be an object.
|
|
830
848
|
if (git !== undefined) {
|
|
831
849
|
this.hosting_deploy_gitDirectory(git);
|
|
850
|
+
if (git.installationUuid !== undefined && (typeof git.installationUuid !== 'string' || git.installationUuid === '')) {
|
|
851
|
+
throw new Error('git.installationUuid must be a non-empty string when provided');
|
|
852
|
+
}
|
|
832
853
|
}
|
|
833
854
|
}
|
|
834
855
|
|
|
@@ -1058,23 +1079,59 @@ class MCPServer {
|
|
|
1058
1079
|
};
|
|
1059
1080
|
}
|
|
1060
1081
|
|
|
1061
|
-
// The Git installation that can reach owner/name (GitLab owners may contain slashes).
|
|
1062
|
-
|
|
1082
|
+
// The Git installation that can reach owner/name (GitLab owners may contain slashes). Every active
|
|
1083
|
+
// installation is checked, not just until the first match: a GitHub and a GitLab mirror can share a
|
|
1084
|
+
// path, the installations come in no set order, and the same request must not deploy one repository
|
|
1085
|
+
// 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
|
|
1086
|
+
// repository past that cannot be found by searching it.
|
|
1087
|
+
async hosting_deploy_findRepository(fullName, installationUuid) {
|
|
1063
1088
|
if (!fullName.includes('/')) {
|
|
1064
1089
|
throw new Error('git.repository must be given as owner/name');
|
|
1065
1090
|
}
|
|
1091
|
+
if (installationUuid !== undefined) {
|
|
1092
|
+
const slash = fullName.lastIndexOf('/');
|
|
1093
|
+
const owner = fullName.slice(0, slash);
|
|
1094
|
+
const repository = fullName.slice(slash + 1);
|
|
1095
|
+
if (owner === '' || repository === '') {
|
|
1096
|
+
throw new Error('git.repository must be given as owner/name');
|
|
1097
|
+
}
|
|
1098
|
+
return { installationUuid, owner, repository };
|
|
1099
|
+
}
|
|
1066
1100
|
const installations = await this.callOperation('hosting_git_list-installations', { status: 'active' });
|
|
1067
1101
|
if (!Array.isArray(installations) || installations.length === 0) {
|
|
1068
1102
|
throw new Error('No active Git installation. Connect GitHub or GitLab once in hPanel (Websites, Manage, Advanced, Git), then try again.');
|
|
1069
1103
|
}
|
|
1104
|
+
// The API returns at most this many repositories per installation. A full list without the
|
|
1105
|
+
// repository may still hold it past the cut, so a match elsewhere cannot be trusted to be the only one.
|
|
1106
|
+
const repositoryLimit = 500;
|
|
1107
|
+
const truncated = [];
|
|
1108
|
+
const matches = [];
|
|
1070
1109
|
for (const installation of installations) {
|
|
1071
|
-
const
|
|
1072
|
-
const
|
|
1110
|
+
const response = await this.callOperation('hosting_git_list-installation-repositories', { uuid: installation.uuid });
|
|
1111
|
+
const repositories = Array.isArray(response) ? response : [];
|
|
1112
|
+
const repo = repositories.find((candidate) => String(candidate.full_name).toLowerCase() === fullName.toLowerCase());
|
|
1073
1113
|
if (repo) {
|
|
1074
|
-
|
|
1114
|
+
matches.push({ installation, installationUuid: installation.uuid, owner: repo.owner, repository: repo.name });
|
|
1115
|
+
} else if (repositories.length >= repositoryLimit) {
|
|
1116
|
+
truncated.push(installation);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
const describe = (installation) => (typeof installation.provider === 'string' ? installation.provider : 'git') + ' account ' + (typeof installation.account_login === 'string' ? installation.account_login : '?') + ' (installation ' + installation.uuid + ')';
|
|
1120
|
+
if (matches.length > 1) {
|
|
1121
|
+
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.');
|
|
1122
|
+
}
|
|
1123
|
+
if (matches.length === 1) {
|
|
1124
|
+
if (truncated.length > 0) {
|
|
1125
|
+
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.');
|
|
1075
1126
|
}
|
|
1127
|
+
const { installationUuid, owner, repository } = matches[0];
|
|
1128
|
+
return { installationUuid, owner, repository };
|
|
1129
|
+
}
|
|
1130
|
+
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).';
|
|
1131
|
+
if (truncated.length > 0) {
|
|
1132
|
+
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.');
|
|
1076
1133
|
}
|
|
1077
|
-
throw new Error(
|
|
1134
|
+
throw new Error(notFound);
|
|
1078
1135
|
}
|
|
1079
1136
|
|
|
1080
1137
|
// What a Git deploy is. There are no local files to look at, so guessing is dangerous: a Node.js
|
|
@@ -1101,7 +1158,15 @@ class MCPServer {
|
|
|
1101
1158
|
if (type === 'nodejs' && directory !== '') {
|
|
1102
1159
|
throw new Error('git.directory is for static deploys: a Node.js build has no target folder');
|
|
1103
1160
|
}
|
|
1104
|
-
|
|
1161
|
+
// On these sites saving auto-deployment settings clones nothing, so a static deploy would report
|
|
1162
|
+
// success and publish nothing.
|
|
1163
|
+
if (type === 'static' && website.website_type === 'nodejs') {
|
|
1164
|
+
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.');
|
|
1165
|
+
}
|
|
1166
|
+
if (type === 'static' && website.website_type === 'builder') {
|
|
1167
|
+
throw new Error('This is a Website Builder website, which cannot deploy from Git: saving Git settings on it does not deploy anything.');
|
|
1168
|
+
}
|
|
1169
|
+
const repo = await this.hosting_deploy_findRepository(git.repository, git.installationUuid);
|
|
1105
1170
|
|
|
1106
1171
|
if (type === 'static') {
|
|
1107
1172
|
// Saving the settings deploys the branch now and again on every push.
|
|
@@ -1121,9 +1186,31 @@ class MCPServer {
|
|
|
1121
1186
|
throw error;
|
|
1122
1187
|
}
|
|
1123
1188
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1189
|
+
// The API can return all-null settings, which mean nothing is stored.
|
|
1190
|
+
const hasStored = stored !== null && typeof stored === 'object' && stored.app_type !== null && stored.app_type !== undefined;
|
|
1191
|
+
let settings;
|
|
1192
|
+
if (hasStored) {
|
|
1193
|
+
settings = this.hosting_deploy_nodeSettings(stored, params, true);
|
|
1194
|
+
} else {
|
|
1195
|
+
// A first deploy: detect the settings from the branch's package.json. The request's settings beat
|
|
1196
|
+
// them, and a different appType drops the build step detected for the other framework.
|
|
1197
|
+
let detected = null;
|
|
1198
|
+
try {
|
|
1199
|
+
detected = await this.callOperation('hosting_nodejs_build-settings-from-repository', {
|
|
1200
|
+
username, domain, owner: repo.owner, repository: repo.repository, branch: git.branch, installation_uuid: repo.installationUuid,
|
|
1201
|
+
});
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
if (!(error instanceof ApiExecutionError && error.status === 422)) {
|
|
1204
|
+
throw error;
|
|
1205
|
+
}
|
|
1206
|
+
if (params.appType === undefined || params.appType === null) {
|
|
1207
|
+
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.');
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
settings = this.hosting_deploy_nodeSettings(detected, params, true);
|
|
1211
|
+
}
|
|
1212
|
+
if (!hasStored || this.hosting_deploy_hasNodeOverrides(params)) {
|
|
1213
|
+
// Later pushes build with the stored settings, not with this request: keep what was given or detected.
|
|
1127
1214
|
await this.callOperation('hosting_nodejs_update-build-settings', { username, domain, ...settings });
|
|
1128
1215
|
}
|
|
1129
1216
|
const build = await this.callOperation('hosting_nodejs_start-build', {
|
|
@@ -1141,10 +1228,11 @@ class MCPServer {
|
|
|
1141
1228
|
// Local checks first, so a bad path fails before any request is made.
|
|
1142
1229
|
const type = this.hosting_deploy_resolveType(params);
|
|
1143
1230
|
const website = await this.findWebsite(domain);
|
|
1144
|
-
return this.hosting_deploy_fromPath(website.username, domain, type, params);
|
|
1231
|
+
return this.hosting_deploy_fromPath(website.username, website.domain, type, params);
|
|
1145
1232
|
}
|
|
1233
|
+
// The lookup ignores case: the API calls and the result use the website's own spelling.
|
|
1146
1234
|
const website = await this.findWebsite(domain);
|
|
1147
|
-
return this.hosting_deploy_fromGit(website, domain, git, params);
|
|
1235
|
+
return this.hosting_deploy_fromGit(website, website.domain, git, params);
|
|
1148
1236
|
}
|
|
1149
1237
|
|
|
1150
1238
|
// The failure analysis re-runs on every call and is limited to 5 a minute, so keep one request per
|
|
@@ -1190,9 +1278,9 @@ class MCPServer {
|
|
|
1190
1278
|
}
|
|
1191
1279
|
|
|
1192
1280
|
async handleHostingGetDeployment(params) {
|
|
1193
|
-
const { domain, buildUuid, fromLine } = params;
|
|
1281
|
+
const { domain: requestedDomain, buildUuid, fromLine } = params;
|
|
1194
1282
|
|
|
1195
|
-
if (!
|
|
1283
|
+
if (!requestedDomain || typeof requestedDomain !== 'string') {
|
|
1196
1284
|
throw new Error('domain is required and must be a string');
|
|
1197
1285
|
}
|
|
1198
1286
|
if (buildUuid !== undefined && (typeof buildUuid !== 'string' || buildUuid === '')) {
|
|
@@ -1202,7 +1290,8 @@ class MCPServer {
|
|
|
1202
1290
|
throw new Error('fromLine must be a non-negative integer when provided');
|
|
1203
1291
|
}
|
|
1204
1292
|
|
|
1205
|
-
|
|
1293
|
+
// The lookup ignores case: from here on use the website's own spelling, so one site has one analysis cache key.
|
|
1294
|
+
const { username, domain } = await this.findWebsite(requestedDomain);
|
|
1206
1295
|
const target = { username, domain };
|
|
1207
1296
|
|
|
1208
1297
|
const gitSettings = await this.callOperation('hosting_git_auto-deployment-settings', target);
|
package/src/core/runtime.ts
CHANGED
|
@@ -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
|
|
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
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
801
|
-
//
|
|
802
|
-
//
|
|
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
|
-
|
|
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
|
|
1109
|
-
const
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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 (!
|
|
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
|
-
|
|
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);
|