hostinger-api-mcp 3.20.1 → 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 CHANGED
@@ -4,6 +4,25 @@ 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
+
7
26
  ## v3.20.1 — 2026-10-28
8
27
 
9
28
  ### Also in this release
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hostinger-api-mcp",
3
- "version": "3.20.1",
3
+ "version": "3.20.3",
4
4
  "description": "Hostinger API — manage domains, DNS, hosting, VPS, billing and more from Gemini CLI.",
5
5
  "mcpServers": {
6
6
  "hostinger": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hostinger-api-mcp",
3
- "version": "3.20.1",
3
+ "version": "3.20.3",
4
4
  "mcpName": "io.github.hostinger/hostinger-api-mcp",
5
5
  "description": "MCP server for Hostinger API",
6
6
  "repository": {
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.1",
9
+ "version": "3.20.3",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@hostinger/mcp",
14
- "version": "3.20.1",
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 { exec } from "child_process";
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
- process.env["OAUTH_ISSUER"] ||
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
- let cmd;
391
- if (process.platform === "darwin") {
392
- cmd = `open "${url}"`;
393
- } else if (process.platform === "win32") {
394
- cmd = `start "" "${url}"`;
395
- } else {
396
- cmd = `xdg-open "${url}"`;
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 { exec } from 'child_process';
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).replace(/\/$/, '');
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
- let cmd: string;
361
- if (process.platform === 'darwin') {
362
- cmd = `open "${url}"`;
363
- } else if (process.platform === 'win32') {
364
- cmd = `start "" "${url}"`;
365
- } else {
366
- cmd = `xdg-open "${url}"`;
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
  }
@@ -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 .env files were not uploaded. Set their values with hosting_nodejs_replace-environment-variables.',
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.hosting_deploy_listFiles(resolved, exclude);
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.hosting_deploy_listFiles(directory);
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 pluginFiles = this.hosting_deployWordpressPlugin_scanDirectory(pluginPath);
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 themeFiles = this.hosting_deployWordpressTheme_scanDirectory(themePath);
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) {
@@ -380,6 +380,93 @@ class MCPServer {
380
380
  }
381
381
  }
382
382
 
383
+ // Directories that hold credentials, the server's own token store (hostinger-mcp) among them.
384
+ private credentialDirectories = ['.ssh', '.aws', '.gnupg', '.config', '.kube', '.docker', '.azure', 'hostinger-mcp'];
385
+
386
+ // System directories. /var as a whole stays deployable: sites live in /var/www, and macOS keeps its temp dir there.
387
+ private 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'];
388
+
389
+ // A deploy publishes what it reads, and the path comes from the model, so prompt-injected arguments could
390
+ // publish any readable file. Refused, before anything is read or sent: the home directory and every directory
391
+ // above it (they contain the credentials), other users' home directories, system directories, a path in a
392
+ // credential directory, and a secret file. Symlinks are resolved first.
393
+ // os is imported by deploy.template: the JS generator dedupes import blocks by their whole text, and this block
394
+ // must stay identical to agency-hosting/shared's, or tus and fs are imported twice.
395
+ private assertDeployablePath(localPath: string): void {
396
+ let real: string;
397
+ try {
398
+ real = fs.realpathSync(localPath);
399
+ } catch (error) {
400
+ throw new Error('Path not found: ' + localPath);
401
+ }
402
+ let home = os.homedir();
403
+ try {
404
+ home = fs.realpathSync(home);
405
+ } catch (error) {
406
+ // a missing home directory has nothing in it to protect
407
+ }
408
+ const inside = (dir: string, file: string): boolean => {
409
+ const relative = path.relative(dir, file);
410
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
411
+ };
412
+ // A home in the filesystem root (HOME=/ in a container run as a user without a passwd entry) exempts nothing.
413
+ const inHome = path.dirname(home) !== home && inside(home, real);
414
+ // The directory that holds the home directories (/Users, /home); not when home sits in the root (/root).
415
+ const homes = path.dirname(home);
416
+ const otherHome = !inHome && path.dirname(homes) !== homes && inside(homes, real);
417
+ const inSystem = !inHome && this.systemDirectories.some((dir: string) => inside(dir, real));
418
+ const inCredentials = real.split(path.sep).some((segment: string) => this.credentialDirectories.includes(segment.toLowerCase()));
419
+ if (inside(real, home) || otherHome || inSystem || inCredentials || (fs.statSync(real).isFile() && this.isSecretFile(path.basename(real)))) {
420
+ throw new Error('Refusing to deploy ' + localPath + ': it is, or contains, a home, system or credentials location. Pass the project directory or archive.');
421
+ }
422
+ }
423
+
424
+ // Env files (examples such as .env.example are kept), credentials and private keys: a deploy would serve them
425
+ // publicly. Matches the CLI's isSecretFile, plus .npmrc and the Hostinger credential files.
426
+ private isSecretFile(name: string): boolean {
427
+ if (['.env', '.envrc', '.netrc', '.git-credentials', '.pgpass', '.npmrc', '.hostinger.yaml', 'credentials.json'].includes(name)) {
428
+ return true;
429
+ }
430
+ if (['id_rsa', 'id_dsa', 'id_ecdsa', 'id_ed25519'].some((key) => name.startsWith(key))) {
431
+ return true;
432
+ }
433
+ if (name.endsWith('.pem') || name.endsWith('.key')) {
434
+ return true;
435
+ }
436
+ return name.startsWith('.env.') && !['.env.example', '.env.sample', '.env.template', '.env.dist'].includes(name);
437
+ }
438
+
439
+ // The files to ship, as sorted forward-slash paths relative to dir, and the secret files left out.
440
+ // node_modules, .git and OS junk are skipped; symlinks are not followed. .gitignore is not read:
441
+ // keep build output out of the directory.
442
+ // exclude is a file that is uploaded on its own (the database dump) and must not also travel inside
443
+ // the archive, where extracting it would leave a second, downloadable copy.
444
+ private listDeployFiles(dir: string, exclude?: string): { files: string[]; skipped: string[] } {
445
+ const files: string[] = [];
446
+ const skipped: string[] = [];
447
+ const walk = (relativeDir: string): void => {
448
+ for (const entry of fs.readdirSync(path.join(dir, relativeDir), { withFileTypes: true })) {
449
+ if (entry.isSymbolicLink()) {
450
+ continue;
451
+ }
452
+ const relativePath = relativeDir ? relativeDir + '/' + entry.name : entry.name;
453
+ if (entry.isDirectory()) {
454
+ if (entry.name !== 'node_modules' && entry.name !== '.git') {
455
+ walk(relativePath);
456
+ }
457
+ } else if (entry.isFile() && entry.name !== '.DS_Store' && path.resolve(dir, relativePath) !== exclude) {
458
+ if (this.isSecretFile(entry.name)) {
459
+ skipped.push(relativePath);
460
+ } else {
461
+ files.push(relativePath);
462
+ }
463
+ }
464
+ }
465
+ };
466
+ walk('');
467
+ return { files: files.sort(), skipped: skipped.sort() };
468
+ }
469
+
383
470
  private async resolveUsername(domain: string): Promise<string> {
384
471
  const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
385
472
  const url = new URL(`api/hosting/v1/websites?domain=${encodeURIComponent(domain)}`, baseUrl).toString();
@@ -718,43 +805,6 @@ class MCPServer {
718
805
  return slug;
719
806
  }
720
807
 
721
- // .env and .env.* hold secrets, and one in a document root can be fetched over HTTP; the platform
722
- // takes variables from its own settings, so they are never packed. Examples are kept.
723
- private hosting_deploy_isEnvFile(name: string): boolean {
724
- return name === '.env' || (name.startsWith('.env.') && !['.env.example', '.env.sample', '.env.template', '.env.dist'].includes(name));
725
- }
726
-
727
- // The files to ship, as sorted forward-slash paths relative to dir, and the .env files left out.
728
- // node_modules, .git and OS junk are skipped; symlinks are not followed. .gitignore is not read:
729
- // keep build output out of the directory.
730
- // exclude is a file that is uploaded on its own (the database dump) and must not also travel inside
731
- // the archive, where extracting it would leave a second, downloadable copy.
732
- private hosting_deploy_listFiles(dir: string, exclude?: string): { files: string[]; skipped: string[] } {
733
- const files: string[] = [];
734
- const skipped: string[] = [];
735
- const walk = (relativeDir: string): void => {
736
- for (const entry of fs.readdirSync(path.join(dir, relativeDir), { withFileTypes: true })) {
737
- if (entry.isSymbolicLink()) {
738
- continue;
739
- }
740
- const relativePath = relativeDir ? relativeDir + '/' + entry.name : entry.name;
741
- if (entry.isDirectory()) {
742
- if (entry.name !== 'node_modules' && entry.name !== '.git') {
743
- walk(relativePath);
744
- }
745
- } else if (entry.isFile() && entry.name !== '.DS_Store' && path.resolve(dir, relativePath) !== exclude) {
746
- if (this.hosting_deploy_isEnvFile(entry.name)) {
747
- skipped.push(relativePath);
748
- } else {
749
- files.push(relativePath);
750
- }
751
- }
752
- }
753
- };
754
- walk('');
755
- return { files: files.sort(), skipped: skipped.sort() };
756
- }
757
-
758
808
  // Adds what was left out to a result, so it is never silent.
759
809
  private hosting_deploy_withSkipped(result: any, skipped: string[]): any {
760
810
  if (skipped.length === 0) {
@@ -763,7 +813,7 @@ class MCPServer {
763
813
  return {
764
814
  ...result,
765
815
  skippedFiles: skipped,
766
- skippedNote: 'These .env files were not uploaded. Set their values with hosting_nodejs_replace-environment-variables.',
816
+ skippedNote: 'These files hold secrets (env files, keys, credentials) and were not uploaded. Set environment values with hosting_nodejs_replace-environment-variables.',
767
817
  };
768
818
  }
769
819
 
@@ -964,7 +1014,7 @@ class MCPServer {
964
1014
  return { archivePath: resolved, skipped: [] };
965
1015
  }
966
1016
 
967
- const { files, skipped } = this.hosting_deploy_listFiles(resolved, exclude);
1017
+ const { files, skipped } = this.listDeployFiles(resolved, exclude);
968
1018
  if (files.length === 0) {
969
1019
  throw new Error('No files to deploy in ' + resolved);
970
1020
  }
@@ -1015,7 +1065,7 @@ class MCPServer {
1015
1065
  const isPlugin = type === 'wordpress-plugin';
1016
1066
  const directory = path.resolve(params.path);
1017
1067
  const slug = params.slug !== undefined ? params.slug : this.hosting_deploy_slug(directory);
1018
- const { files, skipped } = this.hosting_deploy_listFiles(directory);
1068
+ const { files, skipped } = this.listDeployFiles(directory);
1019
1069
  if (files.length === 0) {
1020
1070
  throw new Error('No files found in directory: ' + directory);
1021
1071
  }
@@ -1274,6 +1324,10 @@ class MCPServer {
1274
1324
 
1275
1325
  if (git === undefined) {
1276
1326
  // Local checks first, so a bad path fails before any request is made.
1327
+ this.assertDeployablePath(params.path);
1328
+ if (params.databaseDump !== undefined) {
1329
+ this.assertDeployablePath(params.databaseDump);
1330
+ }
1277
1331
  const type = this.hosting_deploy_resolveType(params);
1278
1332
  const website = await this.findWebsite(domain);
1279
1333
  return this.hosting_deploy_fromPath(website.username, website.domain, type, params);
@@ -1550,6 +1604,7 @@ class MCPServer {
1550
1604
  }
1551
1605
 
1552
1606
  private hosting_importWordpressWebsite_validateArchiveFile(archivePath: string): void {
1607
+ this.assertDeployablePath(archivePath);
1553
1608
  if (!fs.existsSync(archivePath)) {
1554
1609
  throw new Error(`Archive file not found: ${archivePath}`);
1555
1610
  }
@@ -1565,6 +1620,7 @@ class MCPServer {
1565
1620
  }
1566
1621
 
1567
1622
  private hosting_importWordpressWebsite_validateDatabaseFile(databaseDump: string): void {
1623
+ this.assertDeployablePath(databaseDump);
1568
1624
  if (!fs.existsSync(databaseDump)) {
1569
1625
  throw new Error(`Database dump file not found: ${databaseDump}`);
1570
1626
  }
@@ -1820,6 +1876,7 @@ class MCPServer {
1820
1876
  }
1821
1877
 
1822
1878
  private hosting_deployWordpressPlugin_validatePluginDirectory(pluginPath: string): void {
1879
+ this.assertDeployablePath(pluginPath);
1823
1880
  if (!fs.existsSync(pluginPath)) {
1824
1881
  throw new Error(`Plugin directory not found: ${pluginPath}`);
1825
1882
  }
@@ -1830,32 +1887,6 @@ class MCPServer {
1830
1887
  }
1831
1888
  }
1832
1889
 
1833
- private hosting_deployWordpressPlugin_scanDirectory(dirPath: string, basePath: string = dirPath): Array<{absolutePath: string, relativePath: string}> {
1834
- const files: Array<{absolutePath: string, relativePath: string}> = [];
1835
-
1836
- const scanDir = (currentPath: string) => {
1837
- const items = fs.readdirSync(currentPath);
1838
-
1839
- for (const item of items) {
1840
- const itemPath = path.join(currentPath, item);
1841
- const stats = fs.statSync(itemPath);
1842
-
1843
- if (stats.isDirectory()) {
1844
- scanDir(itemPath);
1845
- } else if (stats.isFile()) {
1846
- const relativePath = path.relative(basePath, itemPath);
1847
- files.push({
1848
- absolutePath: itemPath,
1849
- relativePath: relativePath
1850
- });
1851
- }
1852
- }
1853
- };
1854
-
1855
- scanDir(dirPath);
1856
- return files;
1857
- }
1858
-
1859
1890
  private async hosting_deployWordpressPlugin_deployPlugin(username: string, domain: string, slug: string, pluginPath: string): Promise<boolean> {
1860
1891
  const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1861
1892
  const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/plugins/deploy`, baseUrl).toString();
@@ -1920,7 +1951,8 @@ class MCPServer {
1920
1951
  const uploadDirName = `${slug}-${randomSuffix}`;
1921
1952
 
1922
1953
  this.log('info', `Scanning plugin directory: ${pluginPath}`);
1923
- const pluginFiles = this.hosting_deployWordpressPlugin_scanDirectory(pluginPath);
1954
+ const { files, skipped } = this.listDeployFiles(pluginPath);
1955
+ const pluginFiles = files.map((relativePath: string) => ({ absolutePath: path.join(pluginPath, relativePath), relativePath }));
1924
1956
 
1925
1957
  if (pluginFiles.length === 0) {
1926
1958
  throw new Error(`No files found in plugin directory: ${pluginPath}`);
@@ -2008,6 +2040,7 @@ class MCPServer {
2008
2040
  failed: failureCount
2009
2041
  },
2010
2042
  results,
2043
+ ...(skipped.length > 0 ? { skippedFiles: skipped } : {}),
2011
2044
  deploymentError: errorMessage
2012
2045
  };
2013
2046
  }
@@ -2021,6 +2054,7 @@ class MCPServer {
2021
2054
  failed: failureCount
2022
2055
  },
2023
2056
  results,
2057
+ ...(skipped.length > 0 ? { skippedFiles: skipped } : {}),
2024
2058
  uploadDirName
2025
2059
  };
2026
2060
  }
@@ -2051,6 +2085,7 @@ class MCPServer {
2051
2085
  }
2052
2086
 
2053
2087
  private hosting_deployWordpressTheme_validateThemeDirectory(themePath: string): void {
2088
+ this.assertDeployablePath(themePath);
2054
2089
  if (!fs.existsSync(themePath)) {
2055
2090
  throw new Error(`Theme directory not found: ${themePath}`);
2056
2091
  }
@@ -2061,32 +2096,6 @@ class MCPServer {
2061
2096
  }
2062
2097
  }
2063
2098
 
2064
- private hosting_deployWordpressTheme_scanDirectory(dirPath: string, basePath: string = dirPath): Array<{absolutePath: string, relativePath: string}> {
2065
- const files: Array<{absolutePath: string, relativePath: string}> = [];
2066
-
2067
- const scanDir = (currentPath: string) => {
2068
- const items = fs.readdirSync(currentPath);
2069
-
2070
- for (const item of items) {
2071
- const itemPath = path.join(currentPath, item);
2072
- const stats = fs.statSync(itemPath);
2073
-
2074
- if (stats.isDirectory()) {
2075
- scanDir(itemPath);
2076
- } else if (stats.isFile()) {
2077
- const relativePath = path.relative(basePath, itemPath);
2078
- files.push({
2079
- absolutePath: itemPath,
2080
- relativePath: relativePath
2081
- });
2082
- }
2083
- }
2084
- };
2085
-
2086
- scanDir(dirPath);
2087
- return files;
2088
- }
2089
-
2090
2099
  private async hosting_deployWordpressTheme_deployTheme(username: string, domain: string, slug: string, themePath: string, activate: boolean = false): Promise<boolean> {
2091
2100
  const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
2092
2101
  const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/themes/deploy`, baseUrl).toString();
@@ -2152,7 +2161,8 @@ class MCPServer {
2152
2161
  const uploadDirName = `${slug}-${randomSuffix}`;
2153
2162
 
2154
2163
  this.log('info', `Scanning theme directory: ${themePath}`);
2155
- const themeFiles = this.hosting_deployWordpressTheme_scanDirectory(themePath);
2164
+ const { files, skipped } = this.listDeployFiles(themePath);
2165
+ const themeFiles = files.map((relativePath: string) => ({ absolutePath: path.join(themePath, relativePath), relativePath }));
2156
2166
 
2157
2167
  if (themeFiles.length === 0) {
2158
2168
  throw new Error(`No files found in theme directory: ${themePath}`);
@@ -2240,6 +2250,7 @@ class MCPServer {
2240
2250
  failed: failureCount
2241
2251
  },
2242
2252
  results,
2253
+ ...(skipped.length > 0 ? { skippedFiles: skipped } : {}),
2243
2254
  deploymentError: errorMessage
2244
2255
  };
2245
2256
  }
@@ -2253,6 +2264,7 @@ class MCPServer {
2253
2264
  failed: failureCount
2254
2265
  },
2255
2266
  results,
2267
+ ...(skipped.length > 0 ? { skippedFiles: skipped } : {}),
2256
2268
  uploadDirName,
2257
2269
  activated: activate
2258
2270
  };
@@ -2306,6 +2318,7 @@ class MCPServer {
2306
2318
  }
2307
2319
 
2308
2320
  private hosting_deployJsApplication_validateArchiveFile(archivePath: string): void {
2321
+ this.assertDeployablePath(archivePath);
2309
2322
  if (!fs.existsSync(archivePath)) {
2310
2323
  throw new Error(`Archive file not found: ${archivePath}`);
2311
2324
  }
@@ -2601,6 +2614,7 @@ class MCPServer {
2601
2614
  }
2602
2615
 
2603
2616
  private hosting_deployStaticWebsite_validateArchiveFile(archivePath: string): void {
2617
+ this.assertDeployablePath(archivePath);
2604
2618
  if (!fs.existsSync(archivePath)) {
2605
2619
  throw new Error(`Archive file not found: ${archivePath}`);
2606
2620
  }
@@ -3052,6 +3066,7 @@ class MCPServer {
3052
3066
  }
3053
3067
 
3054
3068
  private agencyHosting_validateArchiveFile(archivePath: string): void {
3069
+ this.assertDeployablePath(archivePath);
3055
3070
  if (!fs.existsSync(archivePath)) {
3056
3071
  throw new Error(`Archive file not found: ${archivePath}`);
3057
3072
  }
@@ -3060,6 +3075,12 @@ class MCPServer {
3060
3075
  if (!archiveStats.isFile()) {
3061
3076
  throw new Error(`Archive path is not a file: ${archivePath}`);
3062
3077
  }
3078
+
3079
+ // Only an archive is uploaded (and, with removeArchive, deleted afterwards), never any other file.
3080
+ const name = path.basename(archivePath).toLowerCase();
3081
+ if (!['.zip', '.tar', '.tar.gz', '.tgz', '.7z', '.gz', '.gzip'].some((extension) => name.endsWith(extension))) {
3082
+ throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
3083
+ }
3063
3084
  }
3064
3085
 
3065
3086
  private agencyHosting_removeArchive(archivePath: string, removeArchive: boolean): boolean {
@@ -72,7 +72,7 @@ export default [
72
72
  },
73
73
  "path": {
74
74
  "type": "string",
75
- "description": "Local directory (preferred; it is archived for you, skipping node_modules, .git and .env files, but not reading .gitignore, so keep build output out) or archive (zip, tar, tar.gz, tgz, 7z, gz, gzip). Not together with git."
75
+ "description": "Local directory (preferred; it is archived for you, skipping node_modules, .git and secret files (.env, keys, credentials), but not reading .gitignore, so keep build output out) or archive (zip, tar, tar.gz, tgz, 7z, gz, gzip). Not together with git."
76
76
  },
77
77
  "git": {
78
78
  "type": "object",
@@ -496,7 +496,7 @@ export default [
496
496
  },
497
497
  "pluginPath": {
498
498
  "type": "string",
499
- "description": "Absolute or relative path to the plugin directory containing all plugin files"
499
+ "description": "Absolute or relative path to the plugin directory containing all plugin files. node_modules, .git, symlinks and secret files (.env, keys, credentials) are not uploaded."
500
500
  }
501
501
  },
502
502
  "required": [
@@ -538,7 +538,7 @@ export default [
538
538
  },
539
539
  "themePath": {
540
540
  "type": "string",
541
- "description": "Absolute or relative path to the theme directory containing all theme files"
541
+ "description": "Absolute or relative path to the theme directory containing all theme files. node_modules, .git, symlinks and secret files (.env, keys, credentials) are not uploaded."
542
542
  },
543
543
  "activate": {
544
544
  "type": "boolean",
@@ -89,7 +89,7 @@ const tools: OpenApiTool[] = [
89
89
  },
90
90
  "path": {
91
91
  "type": "string",
92
- "description": "Local directory (preferred; it is archived for you, skipping node_modules, .git and .env files, but not reading .gitignore, so keep build output out) or archive (zip, tar, tar.gz, tgz, 7z, gz, gzip). Not together with git."
92
+ "description": "Local directory (preferred; it is archived for you, skipping node_modules, .git and secret files (.env, keys, credentials), but not reading .gitignore, so keep build output out) or archive (zip, tar, tar.gz, tgz, 7z, gz, gzip). Not together with git."
93
93
  },
94
94
  "git": {
95
95
  "type": "object",
@@ -513,7 +513,7 @@ const tools: OpenApiTool[] = [
513
513
  },
514
514
  "pluginPath": {
515
515
  "type": "string",
516
- "description": "Absolute or relative path to the plugin directory containing all plugin files"
516
+ "description": "Absolute or relative path to the plugin directory containing all plugin files. node_modules, .git, symlinks and secret files (.env, keys, credentials) are not uploaded."
517
517
  }
518
518
  },
519
519
  "required": [
@@ -555,7 +555,7 @@ const tools: OpenApiTool[] = [
555
555
  },
556
556
  "themePath": {
557
557
  "type": "string",
558
- "description": "Absolute or relative path to the theme directory containing all theme files"
558
+ "description": "Absolute or relative path to the theme directory containing all theme files. node_modules, .git, symlinks and secret files (.env, keys, credentials) are not uploaded."
559
559
  },
560
560
  "activate": {
561
561
  "type": "boolean",
@@ -72,7 +72,7 @@ export default [
72
72
  },
73
73
  "path": {
74
74
  "type": "string",
75
- "description": "Local directory (preferred; it is archived for you, skipping node_modules, .git and .env files, but not reading .gitignore, so keep build output out) or archive (zip, tar, tar.gz, tgz, 7z, gz, gzip). Not together with git."
75
+ "description": "Local directory (preferred; it is archived for you, skipping node_modules, .git and secret files (.env, keys, credentials), but not reading .gitignore, so keep build output out) or archive (zip, tar, tar.gz, tgz, 7z, gz, gzip). Not together with git."
76
76
  },
77
77
  "git": {
78
78
  "type": "object",
@@ -416,7 +416,7 @@ export default [
416
416
  },
417
417
  "pluginPath": {
418
418
  "type": "string",
419
- "description": "Absolute or relative path to the plugin directory containing all plugin files"
419
+ "description": "Absolute or relative path to the plugin directory containing all plugin files. node_modules, .git, symlinks and secret files (.env, keys, credentials) are not uploaded."
420
420
  }
421
421
  },
422
422
  "required": [
@@ -458,7 +458,7 @@ export default [
458
458
  },
459
459
  "themePath": {
460
460
  "type": "string",
461
- "description": "Absolute or relative path to the theme directory containing all theme files"
461
+ "description": "Absolute or relative path to the theme directory containing all theme files. node_modules, .git, symlinks and secret files (.env, keys, credentials) are not uploaded."
462
462
  },
463
463
  "activate": {
464
464
  "type": "boolean",
@@ -89,7 +89,7 @@ const tools: OpenApiTool[] = [
89
89
  },
90
90
  "path": {
91
91
  "type": "string",
92
- "description": "Local directory (preferred; it is archived for you, skipping node_modules, .git and .env files, but not reading .gitignore, so keep build output out) or archive (zip, tar, tar.gz, tgz, 7z, gz, gzip). Not together with git."
92
+ "description": "Local directory (preferred; it is archived for you, skipping node_modules, .git and secret files (.env, keys, credentials), but not reading .gitignore, so keep build output out) or archive (zip, tar, tar.gz, tgz, 7z, gz, gzip). Not together with git."
93
93
  },
94
94
  "git": {
95
95
  "type": "object",
@@ -433,7 +433,7 @@ const tools: OpenApiTool[] = [
433
433
  },
434
434
  "pluginPath": {
435
435
  "type": "string",
436
- "description": "Absolute or relative path to the plugin directory containing all plugin files"
436
+ "description": "Absolute or relative path to the plugin directory containing all plugin files. node_modules, .git, symlinks and secret files (.env, keys, credentials) are not uploaded."
437
437
  }
438
438
  },
439
439
  "required": [
@@ -475,7 +475,7 @@ const tools: OpenApiTool[] = [
475
475
  },
476
476
  "themePath": {
477
477
  "type": "string",
478
- "description": "Absolute or relative path to the theme directory containing all theme files"
478
+ "description": "Absolute or relative path to the theme directory containing all theme files. node_modules, .git, symlinks and secret files (.env, keys, credentials) are not uploaded."
479
479
  },
480
480
  "activate": {
481
481
  "type": "boolean",
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/agency-hosting.js';
5
5
  import instructions from '../core/instructions.agency-hosting.js';
6
6
 
7
- startServer({ name: 'hostinger-agency-hosting-mcp', version: '3.20.1', tools, instructions });
7
+ startServer({ name: 'hostinger-agency-hosting-mcp', version: '3.20.3', tools, instructions });
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/agency-hosting.js';
5
5
  import instructions from '../core/instructions.agency-hosting.js';
6
6
 
7
- startServer({ name: 'hostinger-agency-hosting-mcp', version: '3.20.1', tools, instructions });
7
+ startServer({ name: 'hostinger-agency-hosting-mcp', version: '3.20.3', tools, instructions });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/agent-readiness.js';
5
5
 
6
- startServer({ name: 'hostinger-agent-readiness-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-agent-readiness-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/agent-readiness.js';
5
5
 
6
- startServer({ name: 'hostinger-agent-readiness-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-agent-readiness-mcp', version: '3.20.3', tools });
@@ -5,4 +5,4 @@ import tools from '../core/tools/all.js';
5
5
  import instructions from '../core/instructions.js';
6
6
  import skills from '../core/skills.js';
7
7
 
8
- startServer({ name: 'hostinger-api-mcp', version: '3.20.1', tools, instructions, skills });
8
+ startServer({ name: 'hostinger-api-mcp', version: '3.20.3', tools, instructions, skills });
@@ -5,4 +5,4 @@ import tools from '../core/tools/all.js';
5
5
  import instructions from '../core/instructions.js';
6
6
  import skills from '../core/skills.js';
7
7
 
8
- startServer({ name: 'hostinger-api-mcp', version: '3.20.1', tools, instructions, skills });
8
+ startServer({ name: 'hostinger-api-mcp', version: '3.20.3', tools, instructions, skills });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/billing.js';
5
5
 
6
- startServer({ name: 'hostinger-billing-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-billing-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/billing.js';
5
5
 
6
- startServer({ name: 'hostinger-billing-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-billing-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/dns.js';
5
5
 
6
- startServer({ name: 'hostinger-dns-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-dns-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/dns.js';
5
5
 
6
- startServer({ name: 'hostinger-dns-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-dns-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/domains.js';
5
5
 
6
- startServer({ name: 'hostinger-domains-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-domains-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/domains.js';
5
5
 
6
- startServer({ name: 'hostinger-domains-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-domains-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/ecommerce.js';
5
5
 
6
- startServer({ name: 'hostinger-ecommerce-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-ecommerce-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/ecommerce.js';
5
5
 
6
- startServer({ name: 'hostinger-ecommerce-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-ecommerce-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/horizons.js';
5
5
 
6
- startServer({ name: 'hostinger-horizons-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-horizons-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/horizons.js';
5
5
 
6
- startServer({ name: 'hostinger-horizons-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-horizons-mcp', version: '3.20.3', tools });
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/hosting.js';
5
5
  import instructions from '../core/instructions.js';
6
6
 
7
- startServer({ name: 'hostinger-hosting-mcp', version: '3.20.1', tools, instructions });
7
+ startServer({ name: 'hostinger-hosting-mcp', version: '3.20.3', tools, instructions });
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/hosting.js';
5
5
  import instructions from '../core/instructions.js';
6
6
 
7
- startServer({ name: 'hostinger-hosting-mcp', version: '3.20.1', tools, instructions });
7
+ startServer({ name: 'hostinger-hosting-mcp', version: '3.20.3', tools, instructions });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/mail.js';
5
5
 
6
- startServer({ name: 'hostinger-mail-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-mail-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/mail.js';
5
5
 
6
- startServer({ name: 'hostinger-mail-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-mail-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/reach.js';
5
5
 
6
- startServer({ name: 'hostinger-reach-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-reach-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/reach.js';
5
5
 
6
- startServer({ name: 'hostinger-reach-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-reach-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/vps.js';
5
5
 
6
- startServer({ name: 'hostinger-vps-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-vps-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/vps.js';
5
5
 
6
- startServer({ name: 'hostinger-vps-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-vps-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/wordpress.js';
5
5
 
6
- startServer({ name: 'hostinger-wordpress-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-wordpress-mcp', version: '3.20.3', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/wordpress.js';
5
5
 
6
- startServer({ name: 'hostinger-wordpress-mcp', version: '3.20.1', tools });
6
+ startServer({ name: 'hostinger-wordpress-mcp', version: '3.20.3', tools });
package/types.d.ts CHANGED
@@ -14,7 +14,7 @@ export interface APITools {
14
14
  */
15
15
  domain: string;
16
16
  /**
17
- * Local directory (preferred; it is archived for you, skipping node_modules, .git and .env files, but not reading .gitignore, so keep build output out) or archive (zip, tar, tar.gz, tgz, 7z, gz, gzip). Not together with git.
17
+ * Local directory (preferred; it is archived for you, skipping node_modules, .git and secret files (.env, keys, credentials), but not reading .gitignore, so keep build output out) or archive (zip, tar, tar.gz, tgz, 7z, gz, gzip). Not together with git.
18
18
  */
19
19
  path?: string;
20
20
  /**
@@ -230,7 +230,7 @@ export interface APITools {
230
230
  */
231
231
  slug: string;
232
232
  /**
233
- * Absolute or relative path to the plugin directory containing all plugin files
233
+ * Absolute or relative path to the plugin directory containing all plugin files. node_modules, .git, symlinks and secret files (.env, keys, credentials) are not uploaded.
234
234
  */
235
235
  pluginPath: string;
236
236
  };
@@ -251,7 +251,7 @@ export interface APITools {
251
251
  */
252
252
  slug: string;
253
253
  /**
254
- * Absolute or relative path to the theme directory containing all theme files
254
+ * Absolute or relative path to the theme directory containing all theme files. node_modules, .git, symlinks and secret files (.env, keys, credentials) are not uploaded.
255
255
  */
256
256
  themePath: string;
257
257
  /**