hostinger-api-mcp 3.20.0 → 3.20.3
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 +25 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/core/oauth.js +42 -15
- package/src/core/oauth.ts +29 -11
- package/src/core/runtime.js +116 -95
- package/src/core/runtime.ts +116 -95
- package/src/core/tools/all.js +3 -3
- package/src/core/tools/all.ts +3 -3
- package/src/core/tools/hosting.js +3 -3
- package/src/core/tools/hosting.ts +3 -3
- 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 +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ 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.20.3 — 2026-10-28
|
|
8
|
+
|
|
9
|
+
### Operations
|
|
10
|
+
|
|
11
|
+
- Changed the input schema of `hosting_deploy`
|
|
12
|
+
- Changed the input schema of `hosting_deploy-wordpress-plugin`
|
|
13
|
+
- Changed the input schema of `hosting_deploy-wordpress-theme`
|
|
14
|
+
|
|
15
|
+
### Also in this release
|
|
16
|
+
|
|
17
|
+
- Request handling and authentication: `src/core/runtime.js`, `src/core/runtime.ts`
|
|
18
|
+
- Build and packaging: `types.d.ts`
|
|
19
|
+
|
|
20
|
+
## v3.20.2 — 2026-10-28
|
|
21
|
+
|
|
22
|
+
### Also in this release
|
|
23
|
+
|
|
24
|
+
- OAuth sign-in flow: `src/core/oauth.js`, `src/core/oauth.ts`
|
|
25
|
+
|
|
26
|
+
## v3.20.1 — 2026-10-28
|
|
27
|
+
|
|
28
|
+
### Also in this release
|
|
29
|
+
|
|
30
|
+
- Request handling and authentication: `src/core/runtime.js`, `src/core/runtime.ts`
|
|
31
|
+
|
|
7
32
|
## v3.20.0 — 2026-10-28
|
|
8
33
|
|
|
9
34
|
### Operations
|
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.20.
|
|
9
|
+
"version": "3.20.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@hostinger/mcp",
|
|
14
|
-
"version": "3.20.
|
|
14
|
+
"version": "3.20.3",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|
package/src/core/oauth.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash, randomBytes } from "crypto";
|
|
|
2
2
|
import { createServer } from "http";
|
|
3
3
|
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
4
4
|
import { existsSync, readFileSync } from "fs";
|
|
5
|
-
import {
|
|
5
|
+
import { execFile } from "child_process";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import path from "path";
|
|
8
8
|
import os from "os";
|
|
@@ -55,13 +55,41 @@ export function getEnvToken() {
|
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* The issuer must be a bare origin: https, or http only on loopback. It ends up
|
|
60
|
+
* in token requests and in the browser launch, so a path, query or userinfo is
|
|
61
|
+
* refused instead of carried along.
|
|
62
|
+
*/
|
|
63
|
+
function parseIssuer(raw) {
|
|
64
|
+
let u;
|
|
65
|
+
try {
|
|
66
|
+
u = new URL(raw);
|
|
67
|
+
} catch {
|
|
68
|
+
throw new Error(`Invalid OAUTH_ISSUER: ${raw}`);
|
|
69
|
+
}
|
|
70
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(u.hostname);
|
|
71
|
+
const schemeOk =
|
|
72
|
+
u.protocol === "https:" || (u.protocol === "http:" && loopback);
|
|
73
|
+
if (
|
|
74
|
+
!schemeOk ||
|
|
75
|
+
u.username ||
|
|
76
|
+
u.password ||
|
|
77
|
+
u.pathname !== "/" ||
|
|
78
|
+
u.search ||
|
|
79
|
+
u.hash
|
|
80
|
+
) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Invalid OAUTH_ISSUER: ${raw} (expected an https origin such as ${DEFAULT_ISSUER})`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return u.origin;
|
|
86
|
+
}
|
|
87
|
+
|
|
58
88
|
export class OAuthProvider {
|
|
59
89
|
constructor(issuerBaseUrl) {
|
|
60
|
-
this.issuer = (
|
|
61
|
-
issuerBaseUrl ||
|
|
62
|
-
|
|
63
|
-
DEFAULT_ISSUER
|
|
64
|
-
).replace(/\/$/, "");
|
|
90
|
+
this.issuer = parseIssuer(
|
|
91
|
+
issuerBaseUrl || process.env["OAUTH_ISSUER"] || DEFAULT_ISSUER,
|
|
92
|
+
);
|
|
65
93
|
this._loginInProgress = null;
|
|
66
94
|
}
|
|
67
95
|
|
|
@@ -387,15 +415,14 @@ export class OAuthProvider {
|
|
|
387
415
|
process.stderr.write(
|
|
388
416
|
"[OAuth] If the browser does not open, copy the URL above into one manually.\n\n",
|
|
389
417
|
);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
exec(cmd, (err) => {
|
|
418
|
+
// No shell: the URL is a single argv entry, never parsed by sh or cmd.
|
|
419
|
+
const [cmd, args] =
|
|
420
|
+
process.platform === "darwin"
|
|
421
|
+
? ["open", [url]]
|
|
422
|
+
: process.platform === "win32"
|
|
423
|
+
? ["rundll32", ["url.dll,FileProtocolHandler", url]]
|
|
424
|
+
: ["xdg-open", [url]];
|
|
425
|
+
execFile(cmd, args, (err) => {
|
|
399
426
|
if (err) {
|
|
400
427
|
process.stderr.write(
|
|
401
428
|
`[OAuth] Could not auto-launch browser: ${err.message}\n`,
|
package/src/core/oauth.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash, randomBytes } from 'crypto';
|
|
|
2
2
|
import { createServer, Server } from 'http';
|
|
3
3
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
4
4
|
import { existsSync, readFileSync } from 'fs';
|
|
5
|
-
import {
|
|
5
|
+
import { execFile } from 'child_process';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import os from 'os';
|
|
@@ -68,12 +68,32 @@ export function getEnvToken(): string | undefined {
|
|
|
68
68
|
|| process.env['APITOKEN'];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The issuer must be a bare origin: https, or http only on loopback. It ends up
|
|
73
|
+
* in token requests and in the browser launch, so a path, query or userinfo is
|
|
74
|
+
* refused instead of carried along.
|
|
75
|
+
*/
|
|
76
|
+
function parseIssuer(raw: string): string {
|
|
77
|
+
let u: URL;
|
|
78
|
+
try {
|
|
79
|
+
u = new URL(raw);
|
|
80
|
+
} catch {
|
|
81
|
+
throw new Error(`Invalid OAUTH_ISSUER: ${raw}`);
|
|
82
|
+
}
|
|
83
|
+
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(u.hostname);
|
|
84
|
+
const schemeOk = u.protocol === 'https:' || (u.protocol === 'http:' && loopback);
|
|
85
|
+
if (!schemeOk || u.username || u.password || u.pathname !== '/' || u.search || u.hash) {
|
|
86
|
+
throw new Error(`Invalid OAUTH_ISSUER: ${raw} (expected an https origin such as ${DEFAULT_ISSUER})`);
|
|
87
|
+
}
|
|
88
|
+
return u.origin;
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
export class OAuthProvider {
|
|
72
92
|
private readonly issuer: string;
|
|
73
93
|
private _loginInProgress: Promise<string> | null = null;
|
|
74
94
|
|
|
75
95
|
constructor(issuerBaseUrl?: string) {
|
|
76
|
-
this.issuer = (issuerBaseUrl || process.env['OAUTH_ISSUER'] || DEFAULT_ISSUER)
|
|
96
|
+
this.issuer = parseIssuer(issuerBaseUrl || process.env['OAUTH_ISSUER'] || DEFAULT_ISSUER);
|
|
77
97
|
}
|
|
78
98
|
|
|
79
99
|
async getAccessToken(): Promise<string> {
|
|
@@ -357,15 +377,13 @@ export class OAuthProvider {
|
|
|
357
377
|
private _openBrowser(url: string): void {
|
|
358
378
|
process.stderr.write(`\n[OAuth] Opening browser for sign-in:\n ${url}\n`);
|
|
359
379
|
process.stderr.write('[OAuth] If the browser does not open, copy the URL above into one manually.\n\n');
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
exec(cmd, (err) => {
|
|
380
|
+
// No shell: the URL is a single argv entry, never parsed by sh or cmd.
|
|
381
|
+
const [cmd, args]: [string, string[]] = process.platform === 'darwin'
|
|
382
|
+
? ['open', [url]]
|
|
383
|
+
: process.platform === 'win32'
|
|
384
|
+
? ['rundll32', ['url.dll,FileProtocolHandler', url]]
|
|
385
|
+
: ['xdg-open', [url]];
|
|
386
|
+
execFile(cmd, args, (err) => {
|
|
369
387
|
if (err) {
|
|
370
388
|
process.stderr.write(`[OAuth] Could not auto-launch browser: ${err.message}\n`);
|
|
371
389
|
}
|
package/src/core/runtime.js
CHANGED
|
@@ -66,7 +66,7 @@ const GetSkillRequestSchema = z.object({
|
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* MCP Server for Hostinger API
|
|
69
|
-
* Generated from OpenAPI spec version 2.5.
|
|
69
|
+
* Generated from OpenAPI spec version 2.5.1
|
|
70
70
|
*/
|
|
71
71
|
class MCPServer {
|
|
72
72
|
constructor({ name, version, tools, instructions, skills = [] }) {
|
|
@@ -348,6 +348,93 @@ class MCPServer {
|
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
// Directories that hold credentials, the server's own token store (hostinger-mcp) among them.
|
|
352
|
+
credentialDirectories = ['.ssh', '.aws', '.gnupg', '.config', '.kube', '.docker', '.azure', 'hostinger-mcp'];
|
|
353
|
+
|
|
354
|
+
// System directories. /var as a whole stays deployable: sites live in /var/www, and macOS keeps its temp dir there.
|
|
355
|
+
systemDirectories = ['/etc', '/root', '/usr', '/bin', '/sbin', '/boot', '/proc', '/sys', '/dev', '/var/lib', '/var/log', '/var/db', '/var/root', '/private/etc', '/private/var/lib', '/private/var/log', '/private/var/db', '/private/var/root', '/System', '/Library'];
|
|
356
|
+
|
|
357
|
+
// A deploy publishes what it reads, and the path comes from the model, so prompt-injected arguments could
|
|
358
|
+
// publish any readable file. Refused, before anything is read or sent: the home directory and every directory
|
|
359
|
+
// above it (they contain the credentials), other users' home directories, system directories, a path in a
|
|
360
|
+
// credential directory, and a secret file. Symlinks are resolved first.
|
|
361
|
+
// os is imported by deploy.template: the JS generator dedupes import blocks by their whole text, and this block
|
|
362
|
+
// must stay identical to agency-hosting/shared's, or tus and fs are imported twice.
|
|
363
|
+
assertDeployablePath(localPath) {
|
|
364
|
+
let real;
|
|
365
|
+
try {
|
|
366
|
+
real = fs.realpathSync(localPath);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
throw new Error('Path not found: ' + localPath);
|
|
369
|
+
}
|
|
370
|
+
let home = os.homedir();
|
|
371
|
+
try {
|
|
372
|
+
home = fs.realpathSync(home);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
// a missing home directory has nothing in it to protect
|
|
375
|
+
}
|
|
376
|
+
const inside = (dir, file) => {
|
|
377
|
+
const relative = path.relative(dir, file);
|
|
378
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
379
|
+
};
|
|
380
|
+
// A home in the filesystem root (HOME=/ in a container run as a user without a passwd entry) exempts nothing.
|
|
381
|
+
const inHome = path.dirname(home) !== home && inside(home, real);
|
|
382
|
+
// The directory that holds the home directories (/Users, /home); not when home sits in the root (/root).
|
|
383
|
+
const homes = path.dirname(home);
|
|
384
|
+
const otherHome = !inHome && path.dirname(homes) !== homes && inside(homes, real);
|
|
385
|
+
const inSystem = !inHome && this.systemDirectories.some((dir) => inside(dir, real));
|
|
386
|
+
const inCredentials = real.split(path.sep).some((segment) => this.credentialDirectories.includes(segment.toLowerCase()));
|
|
387
|
+
if (inside(real, home) || otherHome || inSystem || inCredentials || (fs.statSync(real).isFile() && this.isSecretFile(path.basename(real)))) {
|
|
388
|
+
throw new Error('Refusing to deploy ' + localPath + ': it is, or contains, a home, system or credentials location. Pass the project directory or archive.');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Env files (examples such as .env.example are kept), credentials and private keys: a deploy would serve them
|
|
393
|
+
// publicly. Matches the CLI's isSecretFile, plus .npmrc and the Hostinger credential files.
|
|
394
|
+
isSecretFile(name) {
|
|
395
|
+
if (['.env', '.envrc', '.netrc', '.git-credentials', '.pgpass', '.npmrc', '.hostinger.yaml', 'credentials.json'].includes(name)) {
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
if (['id_rsa', 'id_dsa', 'id_ecdsa', 'id_ed25519'].some((key) => name.startsWith(key))) {
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
if (name.endsWith('.pem') || name.endsWith('.key')) {
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
return name.startsWith('.env.') && !['.env.example', '.env.sample', '.env.template', '.env.dist'].includes(name);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// The files to ship, as sorted forward-slash paths relative to dir, and the secret files left out.
|
|
408
|
+
// node_modules, .git and OS junk are skipped; symlinks are not followed. .gitignore is not read:
|
|
409
|
+
// keep build output out of the directory.
|
|
410
|
+
// exclude is a file that is uploaded on its own (the database dump) and must not also travel inside
|
|
411
|
+
// the archive, where extracting it would leave a second, downloadable copy.
|
|
412
|
+
listDeployFiles(dir, exclude) {
|
|
413
|
+
const files = [];
|
|
414
|
+
const skipped = [];
|
|
415
|
+
const walk = (relativeDir) => {
|
|
416
|
+
for (const entry of fs.readdirSync(path.join(dir, relativeDir), { withFileTypes: true })) {
|
|
417
|
+
if (entry.isSymbolicLink()) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const relativePath = relativeDir ? relativeDir + '/' + entry.name : entry.name;
|
|
421
|
+
if (entry.isDirectory()) {
|
|
422
|
+
if (entry.name !== 'node_modules' && entry.name !== '.git') {
|
|
423
|
+
walk(relativePath);
|
|
424
|
+
}
|
|
425
|
+
} else if (entry.isFile() && entry.name !== '.DS_Store' && path.resolve(dir, relativePath) !== exclude) {
|
|
426
|
+
if (this.isSecretFile(entry.name)) {
|
|
427
|
+
skipped.push(relativePath);
|
|
428
|
+
} else {
|
|
429
|
+
files.push(relativePath);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
walk('');
|
|
435
|
+
return { files: files.sort(), skipped: skipped.sort() };
|
|
436
|
+
}
|
|
437
|
+
|
|
351
438
|
async resolveUsername(domain) {
|
|
352
439
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
353
440
|
const url = new URL(`api/hosting/v1/websites?domain=${encodeURIComponent(domain)}`, baseUrl).toString();
|
|
@@ -680,43 +767,6 @@ class MCPServer {
|
|
|
680
767
|
return slug;
|
|
681
768
|
}
|
|
682
769
|
|
|
683
|
-
// .env and .env.* hold secrets, and one in a document root can be fetched over HTTP; the platform
|
|
684
|
-
// takes variables from its own settings, so they are never packed. Examples are kept.
|
|
685
|
-
hosting_deploy_isEnvFile(name) {
|
|
686
|
-
return name === '.env' || (name.startsWith('.env.') && !['.env.example', '.env.sample', '.env.template', '.env.dist'].includes(name));
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// The files to ship, as sorted forward-slash paths relative to dir, and the .env files left out.
|
|
690
|
-
// node_modules, .git and OS junk are skipped; symlinks are not followed. .gitignore is not read:
|
|
691
|
-
// keep build output out of the directory.
|
|
692
|
-
// exclude is a file that is uploaded on its own (the database dump) and must not also travel inside
|
|
693
|
-
// the archive, where extracting it would leave a second, downloadable copy.
|
|
694
|
-
hosting_deploy_listFiles(dir, exclude) {
|
|
695
|
-
const files = [];
|
|
696
|
-
const skipped = [];
|
|
697
|
-
const walk = (relativeDir) => {
|
|
698
|
-
for (const entry of fs.readdirSync(path.join(dir, relativeDir), { withFileTypes: true })) {
|
|
699
|
-
if (entry.isSymbolicLink()) {
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
const relativePath = relativeDir ? relativeDir + '/' + entry.name : entry.name;
|
|
703
|
-
if (entry.isDirectory()) {
|
|
704
|
-
if (entry.name !== 'node_modules' && entry.name !== '.git') {
|
|
705
|
-
walk(relativePath);
|
|
706
|
-
}
|
|
707
|
-
} else if (entry.isFile() && entry.name !== '.DS_Store' && path.resolve(dir, relativePath) !== exclude) {
|
|
708
|
-
if (this.hosting_deploy_isEnvFile(entry.name)) {
|
|
709
|
-
skipped.push(relativePath);
|
|
710
|
-
} else {
|
|
711
|
-
files.push(relativePath);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
};
|
|
716
|
-
walk('');
|
|
717
|
-
return { files: files.sort(), skipped: skipped.sort() };
|
|
718
|
-
}
|
|
719
|
-
|
|
720
770
|
// Adds what was left out to a result, so it is never silent.
|
|
721
771
|
hosting_deploy_withSkipped(result, skipped) {
|
|
722
772
|
if (skipped.length === 0) {
|
|
@@ -725,7 +775,7 @@ class MCPServer {
|
|
|
725
775
|
return {
|
|
726
776
|
...result,
|
|
727
777
|
skippedFiles: skipped,
|
|
728
|
-
skippedNote: 'These
|
|
778
|
+
skippedNote: 'These files hold secrets (env files, keys, credentials) and were not uploaded. Set environment values with hosting_nodejs_replace-environment-variables.',
|
|
729
779
|
};
|
|
730
780
|
}
|
|
731
781
|
|
|
@@ -926,7 +976,7 @@ class MCPServer {
|
|
|
926
976
|
return { archivePath: resolved, skipped: [] };
|
|
927
977
|
}
|
|
928
978
|
|
|
929
|
-
const { files, skipped } = this.
|
|
979
|
+
const { files, skipped } = this.listDeployFiles(resolved, exclude);
|
|
930
980
|
if (files.length === 0) {
|
|
931
981
|
throw new Error('No files to deploy in ' + resolved);
|
|
932
982
|
}
|
|
@@ -977,7 +1027,7 @@ class MCPServer {
|
|
|
977
1027
|
const isPlugin = type === 'wordpress-plugin';
|
|
978
1028
|
const directory = path.resolve(params.path);
|
|
979
1029
|
const slug = params.slug !== undefined ? params.slug : this.hosting_deploy_slug(directory);
|
|
980
|
-
const { files, skipped } = this.
|
|
1030
|
+
const { files, skipped } = this.listDeployFiles(directory);
|
|
981
1031
|
if (files.length === 0) {
|
|
982
1032
|
throw new Error('No files found in directory: ' + directory);
|
|
983
1033
|
}
|
|
@@ -1236,6 +1286,10 @@ class MCPServer {
|
|
|
1236
1286
|
|
|
1237
1287
|
if (git === undefined) {
|
|
1238
1288
|
// Local checks first, so a bad path fails before any request is made.
|
|
1289
|
+
this.assertDeployablePath(params.path);
|
|
1290
|
+
if (params.databaseDump !== undefined) {
|
|
1291
|
+
this.assertDeployablePath(params.databaseDump);
|
|
1292
|
+
}
|
|
1239
1293
|
const type = this.hosting_deploy_resolveType(params);
|
|
1240
1294
|
const website = await this.findWebsite(domain);
|
|
1241
1295
|
return this.hosting_deploy_fromPath(website.username, website.domain, type, params);
|
|
@@ -1512,6 +1566,7 @@ class MCPServer {
|
|
|
1512
1566
|
}
|
|
1513
1567
|
|
|
1514
1568
|
hosting_importWordpressWebsite_validateArchiveFile(archivePath) {
|
|
1569
|
+
this.assertDeployablePath(archivePath);
|
|
1515
1570
|
if (!fs.existsSync(archivePath)) {
|
|
1516
1571
|
throw new Error(`Archive file not found: ${archivePath}`);
|
|
1517
1572
|
}
|
|
@@ -1527,6 +1582,7 @@ class MCPServer {
|
|
|
1527
1582
|
}
|
|
1528
1583
|
|
|
1529
1584
|
hosting_importWordpressWebsite_validateDatabaseFile(databaseDump) {
|
|
1585
|
+
this.assertDeployablePath(databaseDump);
|
|
1530
1586
|
if (!fs.existsSync(databaseDump)) {
|
|
1531
1587
|
throw new Error(`Database dump file not found: ${databaseDump}`);
|
|
1532
1588
|
}
|
|
@@ -1782,6 +1838,7 @@ class MCPServer {
|
|
|
1782
1838
|
}
|
|
1783
1839
|
|
|
1784
1840
|
hosting_deployWordpressPlugin_validatePluginDirectory(pluginPath) {
|
|
1841
|
+
this.assertDeployablePath(pluginPath);
|
|
1785
1842
|
if (!fs.existsSync(pluginPath)) {
|
|
1786
1843
|
throw new Error(`Plugin directory not found: ${pluginPath}`);
|
|
1787
1844
|
}
|
|
@@ -1792,32 +1849,6 @@ class MCPServer {
|
|
|
1792
1849
|
}
|
|
1793
1850
|
}
|
|
1794
1851
|
|
|
1795
|
-
hosting_deployWordpressPlugin_scanDirectory(dirPath, basePath = dirPath) {
|
|
1796
|
-
const files = [];
|
|
1797
|
-
|
|
1798
|
-
const scanDir = (currentPath) => {
|
|
1799
|
-
const items = fs.readdirSync(currentPath);
|
|
1800
|
-
|
|
1801
|
-
for (const item of items) {
|
|
1802
|
-
const itemPath = path.join(currentPath, item);
|
|
1803
|
-
const stats = fs.statSync(itemPath);
|
|
1804
|
-
|
|
1805
|
-
if (stats.isDirectory()) {
|
|
1806
|
-
scanDir(itemPath);
|
|
1807
|
-
} else if (stats.isFile()) {
|
|
1808
|
-
const relativePath = path.relative(basePath, itemPath);
|
|
1809
|
-
files.push({
|
|
1810
|
-
absolutePath: itemPath,
|
|
1811
|
-
relativePath: relativePath
|
|
1812
|
-
});
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
};
|
|
1816
|
-
|
|
1817
|
-
scanDir(dirPath);
|
|
1818
|
-
return files;
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
1852
|
async hosting_deployWordpressPlugin_deployPlugin(username, domain, slug, pluginPath) {
|
|
1822
1853
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
1823
1854
|
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/plugins/deploy`, baseUrl).toString();
|
|
@@ -1882,7 +1913,8 @@ class MCPServer {
|
|
|
1882
1913
|
const uploadDirName = `${slug}-${randomSuffix}`;
|
|
1883
1914
|
|
|
1884
1915
|
this.log('info', `Scanning plugin directory: ${pluginPath}`);
|
|
1885
|
-
const
|
|
1916
|
+
const { files, skipped } = this.listDeployFiles(pluginPath);
|
|
1917
|
+
const pluginFiles = files.map((relativePath) => ({ absolutePath: path.join(pluginPath, relativePath), relativePath }));
|
|
1886
1918
|
|
|
1887
1919
|
if (pluginFiles.length === 0) {
|
|
1888
1920
|
throw new Error(`No files found in plugin directory: ${pluginPath}`);
|
|
@@ -1970,6 +2002,7 @@ class MCPServer {
|
|
|
1970
2002
|
failed: failureCount
|
|
1971
2003
|
},
|
|
1972
2004
|
results,
|
|
2005
|
+
...(skipped.length > 0 ? { skippedFiles: skipped } : {}),
|
|
1973
2006
|
deploymentError: errorMessage
|
|
1974
2007
|
};
|
|
1975
2008
|
}
|
|
@@ -1983,6 +2016,7 @@ class MCPServer {
|
|
|
1983
2016
|
failed: failureCount
|
|
1984
2017
|
},
|
|
1985
2018
|
results,
|
|
2019
|
+
...(skipped.length > 0 ? { skippedFiles: skipped } : {}),
|
|
1986
2020
|
uploadDirName
|
|
1987
2021
|
};
|
|
1988
2022
|
}
|
|
@@ -2013,6 +2047,7 @@ class MCPServer {
|
|
|
2013
2047
|
}
|
|
2014
2048
|
|
|
2015
2049
|
hosting_deployWordpressTheme_validateThemeDirectory(themePath) {
|
|
2050
|
+
this.assertDeployablePath(themePath);
|
|
2016
2051
|
if (!fs.existsSync(themePath)) {
|
|
2017
2052
|
throw new Error(`Theme directory not found: ${themePath}`);
|
|
2018
2053
|
}
|
|
@@ -2023,32 +2058,6 @@ class MCPServer {
|
|
|
2023
2058
|
}
|
|
2024
2059
|
}
|
|
2025
2060
|
|
|
2026
|
-
hosting_deployWordpressTheme_scanDirectory(dirPath, basePath = dirPath) {
|
|
2027
|
-
const files = [];
|
|
2028
|
-
|
|
2029
|
-
const scanDir = (currentPath) => {
|
|
2030
|
-
const items = fs.readdirSync(currentPath);
|
|
2031
|
-
|
|
2032
|
-
for (const item of items) {
|
|
2033
|
-
const itemPath = path.join(currentPath, item);
|
|
2034
|
-
const stats = fs.statSync(itemPath);
|
|
2035
|
-
|
|
2036
|
-
if (stats.isDirectory()) {
|
|
2037
|
-
scanDir(itemPath);
|
|
2038
|
-
} else if (stats.isFile()) {
|
|
2039
|
-
const relativePath = path.relative(basePath, itemPath);
|
|
2040
|
-
files.push({
|
|
2041
|
-
absolutePath: itemPath,
|
|
2042
|
-
relativePath: relativePath
|
|
2043
|
-
});
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
};
|
|
2047
|
-
|
|
2048
|
-
scanDir(dirPath);
|
|
2049
|
-
return files;
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
2061
|
async hosting_deployWordpressTheme_deployTheme(username, domain, slug, themePath, activate = false) {
|
|
2053
2062
|
const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
|
|
2054
2063
|
const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/themes/deploy`, baseUrl).toString();
|
|
@@ -2114,7 +2123,8 @@ class MCPServer {
|
|
|
2114
2123
|
const uploadDirName = `${slug}-${randomSuffix}`;
|
|
2115
2124
|
|
|
2116
2125
|
this.log('info', `Scanning theme directory: ${themePath}`);
|
|
2117
|
-
const
|
|
2126
|
+
const { files, skipped } = this.listDeployFiles(themePath);
|
|
2127
|
+
const themeFiles = files.map((relativePath) => ({ absolutePath: path.join(themePath, relativePath), relativePath }));
|
|
2118
2128
|
|
|
2119
2129
|
if (themeFiles.length === 0) {
|
|
2120
2130
|
throw new Error(`No files found in theme directory: ${themePath}`);
|
|
@@ -2202,6 +2212,7 @@ class MCPServer {
|
|
|
2202
2212
|
failed: failureCount
|
|
2203
2213
|
},
|
|
2204
2214
|
results,
|
|
2215
|
+
...(skipped.length > 0 ? { skippedFiles: skipped } : {}),
|
|
2205
2216
|
deploymentError: errorMessage
|
|
2206
2217
|
};
|
|
2207
2218
|
}
|
|
@@ -2215,6 +2226,7 @@ class MCPServer {
|
|
|
2215
2226
|
failed: failureCount
|
|
2216
2227
|
},
|
|
2217
2228
|
results,
|
|
2229
|
+
...(skipped.length > 0 ? { skippedFiles: skipped } : {}),
|
|
2218
2230
|
uploadDirName,
|
|
2219
2231
|
activated: activate
|
|
2220
2232
|
};
|
|
@@ -2268,6 +2280,7 @@ class MCPServer {
|
|
|
2268
2280
|
}
|
|
2269
2281
|
|
|
2270
2282
|
hosting_deployJsApplication_validateArchiveFile(archivePath) {
|
|
2283
|
+
this.assertDeployablePath(archivePath);
|
|
2271
2284
|
if (!fs.existsSync(archivePath)) {
|
|
2272
2285
|
throw new Error(`Archive file not found: ${archivePath}`);
|
|
2273
2286
|
}
|
|
@@ -2563,6 +2576,7 @@ class MCPServer {
|
|
|
2563
2576
|
}
|
|
2564
2577
|
|
|
2565
2578
|
hosting_deployStaticWebsite_validateArchiveFile(archivePath) {
|
|
2579
|
+
this.assertDeployablePath(archivePath);
|
|
2566
2580
|
if (!fs.existsSync(archivePath)) {
|
|
2567
2581
|
throw new Error(`Archive file not found: ${archivePath}`);
|
|
2568
2582
|
}
|
|
@@ -3014,6 +3028,7 @@ class MCPServer {
|
|
|
3014
3028
|
}
|
|
3015
3029
|
|
|
3016
3030
|
agencyHosting_validateArchiveFile(archivePath) {
|
|
3031
|
+
this.assertDeployablePath(archivePath);
|
|
3017
3032
|
if (!fs.existsSync(archivePath)) {
|
|
3018
3033
|
throw new Error(`Archive file not found: ${archivePath}`);
|
|
3019
3034
|
}
|
|
@@ -3022,6 +3037,12 @@ class MCPServer {
|
|
|
3022
3037
|
if (!archiveStats.isFile()) {
|
|
3023
3038
|
throw new Error(`Archive path is not a file: ${archivePath}`);
|
|
3024
3039
|
}
|
|
3040
|
+
|
|
3041
|
+
// Only an archive is uploaded (and, with removeArchive, deleted afterwards), never any other file.
|
|
3042
|
+
const name = path.basename(archivePath).toLowerCase();
|
|
3043
|
+
if (!['.zip', '.tar', '.tar.gz', '.tgz', '.7z', '.gz', '.gzip'].some((extension) => name.endsWith(extension))) {
|
|
3044
|
+
throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
|
|
3045
|
+
}
|
|
3025
3046
|
}
|
|
3026
3047
|
|
|
3027
3048
|
agencyHosting_removeArchive(archivePath, removeArchive) {
|