@hostinger/mcp 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +9 -282
  3. package/gemini-extension.json +1 -1
  4. package/package.json +2 -1
  5. package/server.json +2 -2
  6. package/skills/audit-hosting/SKILL.md +1 -1
  7. package/skills/deploy-to-hosting/SKILL.md +32 -15
  8. package/skills/headless/references/DATABASE.md +3 -3
  9. package/skills/headless/references/DEPLOYMENT.md +10 -12
  10. package/skills/migrate-to-hosting/SKILL.md +3 -3
  11. package/skills/troubleshoot-website/SKILL.md +2 -2
  12. package/src/core/catalog.js +4 -0
  13. package/src/core/catalog.ts +6 -1
  14. package/src/core/instructions.agency-hosting.js +2 -0
  15. package/src/core/instructions.agency-hosting.ts +3 -0
  16. package/src/core/instructions.js +1 -1
  17. package/src/core/instructions.ts +1 -1
  18. package/src/core/runtime.js +1924 -1227
  19. package/src/core/runtime.ts +1890 -1192
  20. package/src/core/skills.js +18 -18
  21. package/src/core/skills.ts +18 -18
  22. package/src/core/tools/agency-hosting.ts +2 -0
  23. package/src/core/tools/agent-readiness.ts +2 -0
  24. package/src/core/tools/all.js +208 -24
  25. package/src/core/tools/all.ts +210 -24
  26. package/src/core/tools/billing.ts +2 -0
  27. package/src/core/tools/dns.ts +2 -0
  28. package/src/core/tools/domains.ts +2 -0
  29. package/src/core/tools/ecommerce.ts +2 -0
  30. package/src/core/tools/horizons.ts +2 -0
  31. package/src/core/tools/hosting.js +202 -21
  32. package/src/core/tools/hosting.ts +204 -21
  33. package/src/core/tools/mail.ts +2 -0
  34. package/src/core/tools/reach.ts +2 -0
  35. package/src/core/tools/vps.ts +2 -0
  36. package/src/core/tools/wordpress.ts +2 -0
  37. package/src/servers/agency-hosting.js +2 -2
  38. package/src/servers/agency-hosting.ts +2 -2
  39. package/src/servers/agent-readiness.js +1 -1
  40. package/src/servers/agent-readiness.ts +1 -1
  41. package/src/servers/all.js +1 -1
  42. package/src/servers/all.ts +1 -1
  43. package/src/servers/billing.js +1 -1
  44. package/src/servers/billing.ts +1 -1
  45. package/src/servers/dns.js +1 -1
  46. package/src/servers/dns.ts +1 -1
  47. package/src/servers/domains.js +1 -1
  48. package/src/servers/domains.ts +1 -1
  49. package/src/servers/ecommerce.js +1 -1
  50. package/src/servers/ecommerce.ts +1 -1
  51. package/src/servers/horizons.js +1 -1
  52. package/src/servers/horizons.ts +1 -1
  53. package/src/servers/hosting.js +1 -1
  54. package/src/servers/hosting.ts +1 -1
  55. package/src/servers/mail.js +1 -1
  56. package/src/servers/mail.ts +1 -1
  57. package/src/servers/reach.js +1 -1
  58. package/src/servers/reach.ts +1 -1
  59. package/src/servers/vps.js +1 -1
  60. package/src/servers/vps.ts +1 -1
  61. package/src/servers/wordpress.js +1 -1
  62. package/src/servers/wordpress.ts +1 -1
  63. package/types.d.ts +86 -0
@@ -21,6 +21,8 @@ import { OAuthProvider, getEnvToken } from "./oauth.js";
21
21
  import * as tus from "tus-js-client";
22
22
  import fs from "fs";
23
23
  import path from "path";
24
+ import os from "os";
25
+ import { Zip, ZipDeflate } from "fflate";
24
26
  import { META_TOOLS, searchOperations, renderSearch, findOperation, runSteps } from "./catalog.js";
25
27
 
26
28
  // Load environment variables
@@ -269,6 +271,9 @@ class MCPServer {
269
271
  */
270
272
  async executeOperation(operation, params) {
271
273
  const tool = findOperation(this.operations, operation, params);
274
+ if (tool.hidden) {
275
+ this.log('warning', `Operation ${tool.name} is hidden from search and will be removed in a future release`);
276
+ }
272
277
  this.log('info', `Executing operation: ${tool.name}`);
273
278
  return tool.custom
274
279
  ? this.executeCustomTool(tool, params)
@@ -293,10 +298,10 @@ class MCPServer {
293
298
 
294
299
  async executeCustomTool(tool, params) {
295
300
  switch (tool.name) {
296
- case 'agency-hosting_deploy-node-static-website':
297
- return await this.handleNodeStaticDeploy(params);
298
- case 'agency-hosting_deploy-php-application':
299
- return await this.handlePhpAppDeploy(params);
301
+ case 'hosting_deploy':
302
+ return await this.handleHostingDeploy(params);
303
+ case 'hosting_get-deployment':
304
+ return await this.handleHostingGetDeployment(params);
300
305
  case 'hosting_import-wordpress-website':
301
306
  return await this.handleWordpressWebsiteImport(params);
302
307
  case 'hosting_deploy-wordpress-plugin':
@@ -311,119 +316,71 @@ class MCPServer {
311
316
  return await this.handleListJavascriptDeployments(params);
312
317
  case 'hosting_show-js-deployment-logs':
313
318
  return await this.handleShowJsDeploymentLogs(params);
319
+ case 'agency-hosting_deploy-node-static-website':
320
+ return await this.handleNodeStaticDeploy(params);
321
+ case 'agency-hosting_deploy-php-application':
322
+ return await this.handlePhpAppDeploy(params);
314
323
  default:
315
324
  throw new Error(`Unknown custom tool: ${tool.name}`);
316
325
  }
317
326
  }
318
327
 
319
- agencyHosting_normalizePath(pathString) {
328
+ normalizePath(pathString) {
320
329
  return pathString.replace(/\\/g, '/');
321
330
  }
322
331
 
323
- agencyHosting_generateUploadDir() {
324
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
325
- let result = '';
326
- for (let i = 0; i < 12; i++) {
327
- result += chars.charAt(Math.floor(Math.random() * chars.length));
328
- }
329
- return result;
330
- }
331
-
332
- agencyHosting_validateRequiredParams(params) {
333
- const { domain, archivePath, removeArchive } = params;
334
-
335
- if (!domain || typeof domain !== 'string') {
336
- throw new Error('domain is required and must be a string');
337
- }
338
-
339
- if (!archivePath || typeof archivePath !== 'string') {
340
- throw new Error('archivePath is required and must be a string');
341
- }
342
-
343
- if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
344
- throw new Error('removeArchive must be a boolean if provided');
345
- }
346
- }
347
-
348
- agencyHosting_validateArchiveFile(archivePath) {
349
- if (!fs.existsSync(archivePath)) {
350
- throw new Error(`Archive file not found: ${archivePath}`);
351
- }
352
-
353
- const archiveStats = fs.statSync(archivePath);
354
- if (!archiveStats.isFile()) {
355
- throw new Error(`Archive path is not a file: ${archivePath}`);
356
- }
357
- }
358
-
359
- agencyHosting_removeArchive(archivePath, removeArchive) {
360
- if (!removeArchive) {
361
- return false;
362
- }
363
-
364
- try {
365
- this.log('info', `Removing archive file: ${archivePath}`);
366
- fs.unlinkSync(archivePath);
367
- this.log('info', `Successfully removed archive file: ${archivePath}`);
368
- return true;
369
- } catch (error) {
370
- const errorMessage = error instanceof Error ? error.message : String(error);
371
- this.log('error', `Failed to remove archive file: ${errorMessage}`);
372
- return false;
332
+ // The path becomes part of a URL, where a . or .. segment resolves outside its directory. Separators are
333
+ // normalized first, because a backslash in a file name on Linux or macOS turns into one.
334
+ assertSafeUploadPath(relativePath) {
335
+ const segments = this.normalizePath(relativePath).split('/');
336
+ if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
337
+ throw new Error('Unsafe upload path: ' + relativePath);
373
338
  }
374
339
  }
375
340
 
376
- async agencyHosting_resolveWebsiteUid(domain) {
341
+ async resolveUsername(domain) {
377
342
  const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
378
- const perPage = 100;
379
-
343
+ const url = new URL(`api/hosting/v1/websites?domain=${encodeURIComponent(domain)}`, baseUrl).toString();
344
+
380
345
  try {
381
346
  const bearerToken = await this.getAuthToken();
382
- let page = 1;
383
- let total = 0;
384
-
385
- do {
386
- const url = new URL(
387
- `api/agency-hosting/v1/domains?page=${page}&per_page=${perPage}`,
388
- baseUrl
389
- ).toString();
390
-
391
- const config = {
392
- method: 'get',
393
- url,
394
- headers: {
395
- ...this.headers,
396
- 'Authorization': `Bearer ${bearerToken}`
397
- },
398
- timeout: 60000, // 60s
399
- validateStatus: function (status) {
400
- return status < 500;
401
- }
402
- };
403
-
404
- const response = await axios(config);
405
-
406
- if (response.status !== 200) {
407
- throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
408
- }
409
-
410
- const domains = response.data?.data ?? [];
411
- const match = domains.find((item) => item.fqdn === domain);
412
- if (match?.website_uid) {
413
- this.log('info', `Resolved website UID: ${match.website_uid} for domain: ${domain}`);
414
- return match.website_uid;
347
+
348
+ const config = {
349
+ method: 'get',
350
+ url,
351
+ headers: {
352
+ ...this.headers,
353
+ 'Authorization': `Bearer ${bearerToken}`
354
+ },
355
+ timeout: 60000, // 60s
356
+ validateStatus: function (status) {
357
+ return status < 500;
415
358
  }
416
-
417
- total = response.data?.meta?.total ?? domains.length;
418
- page++;
419
- } while ((page - 1) * perPage < total);
420
-
421
- throw new Error(`No Agency Plan website found for domain: ${domain}`);
422
-
359
+ };
360
+
361
+ const response = await axios(config);
362
+
363
+ if (response.status !== 200) {
364
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
365
+ }
366
+
367
+ const websites = response.data?.data;
368
+ if (!websites || websites.length === 0) {
369
+ throw new Error(`No website found for domain: ${domain}`);
370
+ }
371
+
372
+ const username = websites[0].username;
373
+ if (!username) {
374
+ throw new Error(`Username not found in website data for domain: ${domain}`);
375
+ }
376
+
377
+ this.log('info', `Resolved username: ${username} for domain: ${domain}`);
378
+ return username;
379
+
423
380
  } catch (error) {
424
381
  const errorMessage = error instanceof Error ? error.message : String(error);
425
- this.log('error', `Failed to resolve website UID for domain ${domain}: ${errorMessage}`);
426
-
382
+ this.log('error', `Failed to resolve username for domain ${domain}: ${errorMessage}`);
383
+
427
384
  if (axios.isAxiosError(error)) {
428
385
  const responseData = error.response?.data;
429
386
  const responseStatus = error.response?.status;
@@ -432,14 +389,14 @@ class MCPServer {
432
389
  data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
433
390
  });
434
391
  }
435
-
392
+
436
393
  throw error;
437
394
  }
438
395
  }
439
396
 
440
- async agencyHosting_fetchUploadCredentials(websiteUid) {
397
+ async fetchUploadCredentials(username, domain) {
441
398
  const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
442
- const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/files/upload-urls`, baseUrl).toString();
399
+ const url = new URL('api/hosting/v1/files/upload-urls', baseUrl).toString();
443
400
 
444
401
  try {
445
402
  const bearerToken = await this.getAuthToken();
@@ -452,6 +409,10 @@ class MCPServer {
452
409
  'Authorization': `Bearer ${bearerToken}`,
453
410
  'Content-Type': 'application/json'
454
411
  },
412
+ data: {
413
+ username,
414
+ domain
415
+ },
455
416
  timeout: 60000, // 60s
456
417
  validateStatus: function (status) {
457
418
  return status < 500;
@@ -483,14 +444,17 @@ class MCPServer {
483
444
  }
484
445
  }
485
446
 
486
- async agencyHosting_uploadFile(filePath, relativePath, uploadUrl, authRestToken, authToken) {
447
+ async uploadFile(filePath, relativePath, uploadUrl, authRestToken, authToken) {
487
448
  return new Promise(async (resolve, reject) => {
488
449
  try {
489
450
  const stats = fs.statSync(filePath);
490
451
  const fileStream = fs.createReadStream(filePath);
491
-
452
+
453
+ this.assertSafeUploadPath(relativePath);
492
454
  const cleanUploadUrl = uploadUrl.replace(new RegExp('/$'), '');
493
- const normalizedPath = this.agencyHosting_normalizePath(relativePath);
455
+ // The path is part of a URL: encode each segment so a #, ? or % in a file name is not read as
456
+ // a fragment, a query or an escape, and keep the / separators.
457
+ const normalizedPath = this.normalizePath(relativePath).split('/').map((segment) => encodeURIComponent(segment)).join('/');
494
458
  const uploadUrlWithFile = `${cleanUploadUrl}/${normalizedPath}?override=true`;
495
459
 
496
460
  const requestHeaders = {
@@ -511,13 +475,13 @@ class MCPServer {
511
475
  });
512
476
  } catch (error) {
513
477
  const errorMessage = error instanceof Error ? error.message : String(error);
514
-
478
+
515
479
  if (axios.isAxiosError(error)) {
516
480
  const responseData = error.response?.data;
517
481
  const responseStatus = error.response?.status;
518
482
  const responseHeaders = error.response?.headers;
519
483
  const responseText = typeof responseData === 'object' ? JSON.stringify(responseData) : responseData;
520
-
484
+
521
485
  this.log('error', 'Pre-upload POST request failed - Full Response Details:', {
522
486
  status: responseStatus,
523
487
  headers: responseHeaders,
@@ -552,7 +516,7 @@ class MCPServer {
552
516
  onSuccess: () => {
553
517
  this.log('info', `TUS upload completed for ${relativePath}`, { url: upload.url });
554
518
  resolve({
555
- url: upload.url || uploadUrlWithFile,
519
+ url: upload.url,
556
520
  filename: relativePath
557
521
  });
558
522
  }
@@ -568,474 +532,680 @@ class MCPServer {
568
532
  });
569
533
  }
570
534
 
571
- async agencyHosting_deployNodeStatic_triggerBuildAssets(websiteUid, archivePath) {
572
- const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
573
- const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/build-assets`, baseUrl).toString();
535
+ // Run a catalog operation by name through the runtime's own request code (auth,
536
+ // path parameters, API errors). Hidden operations are still in the catalog, and
537
+ // calling them here does not log the deprecation warning that execute does.
538
+ async callOperation(name, params) {
539
+ const tool = this.operations.get(name);
540
+ if (!tool) {
541
+ throw new Error(`Operation ${name} is not available in this server`);
542
+ }
543
+ return this.executeApiCall(tool, params);
544
+ }
574
545
 
575
- try {
576
- const bearerToken = await this.getAuthToken();
546
+ // The website for a domain, matched exactly (the list endpoint filters by substring).
547
+ async findWebsite(domain) {
548
+ const response = await this.callOperation('hosting_websites_list', { domain });
549
+ const sites = response && Array.isArray(response.data) ? response.data : [];
550
+ const site = sites.find((website) => website.domain && website.domain.toLowerCase() === domain.toLowerCase());
551
+ if (!site) {
552
+ throw new Error('No website found for domain: ' + domain);
553
+ }
554
+ return site;
555
+ }
577
556
 
578
- const config = {
579
- method: 'post',
580
- url,
581
- headers: {
582
- ...this.headers,
583
- 'Authorization': `Bearer ${bearerToken}`,
584
- 'Content-Type': 'application/json'
585
- },
586
- data: {
587
- archive_path: archivePath
588
- },
589
- timeout: 60000,
590
- validateStatus: function (status) {
591
- return status < 500;
592
- }
593
- };
557
+ hosting_deploy_types = ['static', 'nodejs', 'wordpress-site', 'wordpress-plugin', 'wordpress-theme'];
594
558
 
595
- const response = await axios(config);
559
+ // A package.json that depends on one of these is an app with a server, not dev tooling.
560
+ hosting_deploy_serverDependencies = ['express', 'fastify', 'koa', 'hono', 'next', 'nuxt', '@nestjs/core', '@remix-run/node', '@sveltejs/kit', 'astro'];
596
561
 
597
- if (response.status < 200 || response.status >= 300) {
598
- throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
599
- }
562
+ // WordPress reads plugin and theme headers from the first 8 KB of a file.
563
+ hosting_deploy_head(file) {
564
+ const fd = fs.openSync(file, 'r');
565
+ try {
566
+ const buffer = Buffer.alloc(8192);
567
+ const read = fs.readSync(fd, buffer, 0, 8192, 0);
568
+ return buffer.toString('utf8', 0, read).toLowerCase();
569
+ } finally {
570
+ fs.closeSync(fd);
571
+ }
572
+ }
600
573
 
601
- this.log('info', `Successfully triggered build for website ${websiteUid}`);
602
- return response.data;
574
+ // What a directory is, from files in its top level. WordPress signals win over package.json,
575
+ // because plugins and themes routinely carry one for their assets. Two WordPress signals at
576
+ // once are ambiguous and need an explicit type.
577
+ hosting_deploy_detectType(dir) {
578
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
579
+ throw new Error('Directory not found: ' + dir);
580
+ }
581
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
582
+ const hasFile = (name) => entries.some((entry) => entry.isFile() && entry.name === name);
603
583
 
604
- } catch (error) {
605
- const errorMessage = error instanceof Error ? error.message : String(error);
606
- this.log('error', `Failed to trigger build: ${errorMessage}`);
584
+ const wordpress = [];
585
+ if (entries.some((entry) => entry.isFile() && entry.name.endsWith('.php') && this.hosting_deploy_head(path.join(dir, entry.name)).includes('plugin name:'))) {
586
+ wordpress.push('wordpress-plugin');
587
+ }
588
+ if (hasFile('style.css') && this.hosting_deploy_head(path.join(dir, 'style.css')).includes('theme name:')) {
589
+ wordpress.push('wordpress-theme');
590
+ }
591
+ if (hasFile('wp-config.php') || entries.some((entry) => entry.isDirectory() && entry.name === 'wp-content')) {
592
+ wordpress.push('wordpress-site');
593
+ }
594
+ if (wordpress.length > 1) {
595
+ throw new Error('Ambiguous directory: it looks like ' + wordpress.join(' and ') + '. Pass type to choose.');
596
+ }
597
+ if (wordpress.length === 1) {
598
+ return wordpress[0];
599
+ }
607
600
 
608
- if (axios.isAxiosError(error)) {
609
- const responseData = error.response?.data;
610
- const responseStatus = error.response?.status;
611
- this.log('error', 'API Error Details:', {
612
- status: responseStatus,
613
- data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
614
- });
601
+ let pkg = null;
602
+ const manifest = path.join(dir, 'package.json');
603
+ if (fs.existsSync(manifest)) {
604
+ try {
605
+ pkg = JSON.parse(fs.readFileSync(manifest, 'utf8'));
606
+ } catch (error) {
607
+ throw new Error('package.json could not be read (' + (error instanceof Error ? error.message : String(error)) + '): fix it, or pass type to skip detection');
608
+ }
609
+ }
610
+ if (pkg) {
611
+ const dependencies = pkg.dependencies || {};
612
+ const hasServer = this.hosting_deploy_serverDependencies.some((name) => dependencies[name] !== undefined);
613
+ if ((pkg.scripts && (pkg.scripts.build || pkg.scripts.start)) || hasServer) {
614
+ return 'nodejs';
615
615
  }
616
-
617
- throw error;
618
616
  }
617
+ return 'static';
619
618
  }
620
619
 
621
- async handleNodeStaticDeploy(params) {
622
- const { domain, archivePath, removeArchive = true } = params;
623
-
624
- this.agencyHosting_validateRequiredParams(params);
625
- this.agencyHosting_validateArchiveFile(archivePath);
626
-
627
- this.log('info', `Resolving website UID from domain: ${domain}`);
628
- const websiteUid = await this.agencyHosting_resolveWebsiteUid(domain);
629
-
630
- this.log('info', `Starting archive upload for ${domain}`);
631
-
632
- let uploadCredentials;
620
+ // The explicit type if given, else detected from a directory. Archives cannot be inspected.
621
+ hosting_deploy_resolveType(params) {
622
+ const { path: sourcePath, type, databaseDump } = params;
623
+ let stats;
633
624
  try {
634
- uploadCredentials = await this.agencyHosting_fetchUploadCredentials(websiteUid);
625
+ stats = fs.statSync(sourcePath);
635
626
  } catch (error) {
636
- const errorMessage = error instanceof Error ? error.message : String(error);
637
- throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
627
+ throw new Error('Path not found: ' + sourcePath);
638
628
  }
639
629
 
640
- const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
630
+ if (type !== undefined && !this.hosting_deploy_types.includes(type)) {
631
+ throw new Error('type must be one of: ' + this.hosting_deploy_types.join(', '));
632
+ }
633
+ let resolved = type;
634
+ if (resolved === undefined) {
635
+ if (!stats.isDirectory()) {
636
+ throw new Error('The path is an archive, which cannot be inspected: pass type (static, nodejs or wordpress-site), or pass the directory to detect it');
637
+ }
638
+ resolved = this.hosting_deploy_detectType(sourcePath);
639
+ }
640
+ if ((resolved === 'wordpress-plugin' || resolved === 'wordpress-theme') && !stats.isDirectory()) {
641
+ throw new Error('A ' + (resolved === 'wordpress-plugin' ? 'plugin' : 'theme') + ' deploy needs a directory, not a file');
642
+ }
643
+ if (resolved === 'wordpress-site' && !databaseDump) {
644
+ throw new Error('databaseDump (path to a .sql file) is required to import a WordPress site');
645
+ }
646
+ return resolved;
647
+ }
641
648
 
642
- if (!uploadUrl || !authToken || !authRestToken) {
643
- throw new Error('Invalid upload credentials received from API');
649
+ // A WordPress slug from a directory name: lower case letters, digits, "-" and "_".
650
+ hosting_deploy_slug(dir) {
651
+ const base = path.basename(dir);
652
+ const slug = base.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
653
+ if (!slug) {
654
+ throw new Error('Cannot derive a slug from "' + base + '"; pass slug');
644
655
  }
656
+ return slug;
657
+ }
645
658
 
646
- const archiveBasename = path.basename(archivePath);
647
- const uploadDir = `.h5g/${this.agencyHosting_generateUploadDir()}`;
648
- let uploadResult;
649
- try {
650
- uploadResult = await this.agencyHosting_uploadFile(
651
- archivePath,
652
- `${uploadDir}/${archiveBasename}`,
653
- uploadUrl,
654
- authRestToken,
655
- authToken
656
- );
659
+ // .env and .env.* hold secrets, and one in a document root can be fetched over HTTP; the platform
660
+ // takes variables from its own settings, so they are never packed. Examples are kept.
661
+ hosting_deploy_isEnvFile(name) {
662
+ return name === '.env' || (name.startsWith('.env.') && !['.env.example', '.env.sample', '.env.template', '.env.dist'].includes(name));
663
+ }
657
664
 
658
- this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
659
- } catch (error) {
660
- const errorMessage = error instanceof Error ? error.message : String(error);
661
- throw new Error(`Failed to upload archive: ${errorMessage}`);
665
+ // The files to ship, as sorted forward-slash paths relative to dir, and the .env files left out.
666
+ // node_modules, .git and OS junk are skipped; symlinks are not followed. .gitignore is not read:
667
+ // keep build output out of the directory.
668
+ // exclude is a file that is uploaded on its own (the database dump) and must not also travel inside
669
+ // the archive, where extracting it would leave a second, downloadable copy.
670
+ hosting_deploy_listFiles(dir, exclude) {
671
+ const files = [];
672
+ const skipped = [];
673
+ const walk = (relativeDir) => {
674
+ for (const entry of fs.readdirSync(path.join(dir, relativeDir), { withFileTypes: true })) {
675
+ if (entry.isSymbolicLink()) {
676
+ continue;
677
+ }
678
+ const relativePath = relativeDir ? relativeDir + '/' + entry.name : entry.name;
679
+ if (entry.isDirectory()) {
680
+ if (entry.name !== 'node_modules' && entry.name !== '.git') {
681
+ walk(relativePath);
682
+ }
683
+ } else if (entry.isFile() && entry.name !== '.DS_Store' && path.resolve(dir, relativePath) !== exclude) {
684
+ if (this.hosting_deploy_isEnvFile(entry.name)) {
685
+ skipped.push(relativePath);
686
+ } else {
687
+ files.push(relativePath);
688
+ }
689
+ }
690
+ }
691
+ };
692
+ walk('');
693
+ return { files: files.sort(), skipped: skipped.sort() };
694
+ }
695
+
696
+ // Adds what was left out to a result, so it is never silent.
697
+ hosting_deploy_withSkipped(result, skipped) {
698
+ if (skipped.length === 0) {
699
+ return result;
662
700
  }
701
+ return {
702
+ ...result,
703
+ skippedFiles: skipped,
704
+ skippedNote: 'These .env files were not uploaded. Set their values with hosting_nodejs_replace-environment-variables.',
705
+ };
706
+ }
663
707
 
664
- let deployResult;
708
+ async hosting_deploy_createZip(dir, files, dest) {
709
+ const out = fs.createWriteStream(dest);
710
+ const finished = new Promise ((resolve, reject) => {
711
+ out.on('finish', () => resolve());
712
+ out.on('error', reject);
713
+ });
714
+ // The error is reported by the final await; this keeps an early one from being an unhandled rejection.
715
+ finished.catch(() => {});
716
+ const zip = new Zip((error, chunk, final) => {
717
+ if (error) {
718
+ out.destroy(error);
719
+ return;
720
+ }
721
+ out.write(chunk);
722
+ if (final) {
723
+ out.end();
724
+ }
725
+ });
726
+ // When the output stream is full, stop reading and compressing until it drains, so a large site cannot
727
+ // pile up in memory; a write error ends the wait.
728
+ const drained = async () => {
729
+ if (out.writableNeedDrain) {
730
+ await Promise.race([new Promise ((resolve) => out.once('drain', () => resolve())), finished]);
731
+ }
732
+ };
665
733
  try {
666
- this.log('info', `Triggering build for ${domain}`);
667
- deployResult = await this.agencyHosting_deployNodeStatic_triggerBuildAssets(websiteUid, uploadDir);
668
- } catch (error) {
669
- const errorMessage = error instanceof Error ? error.message : String(error);
670
- this.log('error', `Failed to trigger build: ${errorMessage}`);
671
- const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
672
-
673
- return {
674
- upload: {
675
- status: 'success',
676
- data: {
677
- filename: uploadResult.filename
734
+ for (const relativePath of files) {
735
+ const entry = new ZipDeflate(relativePath, { level: 6 });
736
+ zip.add(entry);
737
+ for await (const chunk of fs.createReadStream(path.join(dir, relativePath))) {
738
+ entry.push(chunk);
739
+ await drained();
740
+ if (out.destroyed) {
741
+ break;
678
742
  }
679
- },
680
- deploy: {
681
- status: 'error',
682
- error: errorMessage
683
- },
684
- removeArchive: {
685
- status: archiveRemoved ? 'success' : 'skipped'
686
743
  }
687
- };
688
- }
689
-
690
- const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
691
-
692
- return {
693
- upload: {
694
- status: 'success',
695
- data: {
696
- filename: uploadResult.filename
744
+ if (out.destroyed) {
745
+ break;
697
746
  }
698
- },
699
- deploy: {
700
- status: 'success',
701
- data: deployResult
702
- },
703
- removeArchive: {
704
- status: archiveRemoved ? 'success' : 'skipped'
747
+ entry.push(new Uint8Array(0), true);
748
+ await drained();
705
749
  }
750
+ zip.end();
751
+ } catch (error) {
752
+ out.destroy(error);
753
+ }
754
+ await finished;
755
+ }
756
+
757
+ // The start-build fields: explicit overrides (the tool's camelCase params) beat base, which is
758
+ // the settings detected from an archive or stored for the site. Only the framework is required.
759
+ // An app that runs straight from source (plain-JavaScript Express) has no build script or output
760
+ // directory, and the API stores them as null, so a missing one is sent as null. Detection supplies
761
+ // the build script but only guesses a root-level entry file, so a compiled app (TypeScript) must
762
+ // pass entryFile.
763
+ // storedSettings tells base apart: settings stored for the site (Git deploy) belong to whatever
764
+ // framework it ran before, so a different appType starts from a clean build step, while settings
765
+ // detected from the project itself (archive deploy) are always right to keep.
766
+ hosting_deploy_nodeSettings(base, params, storedSettings) {
767
+ const given = base || {};
768
+ const appType = params.appType ?? given.app_type;
769
+ const sameFramework = !storedSettings || params.appType === undefined || params.appType === given.app_type;
770
+ const stored = sameFramework ? given : { node_version: given.node_version, root_directory: given.root_directory, package_manager: given.package_manager };
771
+ if (appType === undefined || appType === null) {
772
+ 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
+ }
774
+ const optional = {
775
+ entry_file: params.entryFile ?? stored.entry_file,
776
+ package_manager: params.packageManager ?? stored.package_manager,
777
+ };
778
+ return {
779
+ node_version: params.nodeVersion ?? stored.node_version ?? 22,
780
+ app_type: appType,
781
+ 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,
784
+ ...Object.fromEntries(Object.entries(optional).filter(([, value]) => value !== undefined && value !== null)),
706
785
  };
707
786
  }
708
787
 
709
- async agencyHosting_deployPhpApp_triggerImportArchive(websiteUid, archiveName) {
710
- const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
711
- const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/files/import-archive`, baseUrl).toString();
788
+ hosting_deploy_hasNodeOverrides(params) {
789
+ return ['nodeVersion', 'appType', 'rootDirectory', 'outputDirectory', 'buildScript', 'entryFile', 'packageManager'].some((name) => params[name] !== undefined);
790
+ }
712
791
 
713
- try {
714
- const bearerToken = await this.getAuthToken();
792
+ // The folder of the site a static Git deploy goes into, as a clean relative path ('' is the document root).
793
+ // Separators are normalized first, because some servers read a backslash as one: a value made of dots and
794
+ // backslashes, or one that starts with a drive letter, must not get past as a name.
795
+ hosting_deploy_gitDirectory(git) {
796
+ if (git.directory === undefined || git.directory === '') {
797
+ return '';
798
+ }
799
+ let directory = typeof git.directory === 'string' ? this.normalizePath(git.directory) : '';
800
+ while (directory.length > 1 && directory.endsWith('/')) {
801
+ directory = directory.slice(0, -1);
802
+ }
803
+ const driveRooted = directory.length > 1 && directory[1] === ':' && /[A-Za-z]/.test(directory[0]);
804
+ if (directory === '' || directory.startsWith('/') || driveRooted || directory.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
805
+ throw new Error('git.directory must be a relative path inside the website, without ..');
806
+ }
807
+ return directory;
808
+ }
715
809
 
716
- const config = {
717
- method: 'post',
718
- url,
719
- headers: {
720
- ...this.headers,
721
- 'Authorization': `Bearer ${bearerToken}`,
722
- 'Content-Type': 'application/json'
723
- },
724
- data: {
725
- archive_name: archiveName
726
- },
727
- timeout: 60000,
728
- validateStatus: function (status) {
729
- return status < 500;
730
- }
731
- };
810
+ hosting_deploy_validate(params) {
811
+ const { domain, path: sourcePath, git } = params;
732
812
 
733
- const response = await axios(config);
813
+ if (!domain || typeof domain !== 'string') {
814
+ throw new Error('domain is required and must be a string');
815
+ }
816
+ if ((sourcePath === undefined) === (git === undefined)) {
817
+ throw new Error('Pass exactly one of path (a local directory or archive) or git ({ repository, branch })');
818
+ }
819
+ if (sourcePath !== undefined && (typeof sourcePath !== 'string' || sourcePath === '')) {
820
+ throw new Error('path must be a non-empty string');
821
+ }
822
+ // The slug becomes part of the remote upload path, so it cannot carry path or URL syntax.
823
+ if (params.slug !== undefined && !(typeof params.slug === 'string' && /^[a-z0-9_-]+$/.test(params.slug))) {
824
+ throw new Error('slug must contain only lower-case letters, digits, "-" and "_"');
825
+ }
826
+ if (git !== undefined && (typeof git !== 'object' || git === null || typeof git.repository !== 'string' || typeof git.branch !== 'string' || git.branch === '')) {
827
+ throw new Error('git must be an object with repository (owner/name) and branch');
828
+ }
829
+ // Read only now that git is known to be an object.
830
+ if (git !== undefined) {
831
+ this.hosting_deploy_gitDirectory(git);
832
+ }
833
+ }
734
834
 
735
- if (response.status < 200 || response.status >= 300) {
736
- throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
835
+ hosting_deploy_timestamp() {
836
+ const pad = (value) => String(value).padStart(2, '0');
837
+ const now = new Date();
838
+ return String(now.getFullYear()) + pad(now.getMonth() + 1) + pad(now.getDate()) + '_' + pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds());
839
+ }
840
+
841
+ // The WordPress deploy endpoints, called directly. hosting_deploy also runs in hostinger-hosting-mcp,
842
+ // whose catalog holds the hosting group only, so it cannot look these operations up.
843
+ hosting_deploy_wordpressEndpoints = {
844
+ import: '/api/hosting/v1/accounts/{username}/websites/{domain}/wordpress/import',
845
+ plugin: '/api/hosting/v1/accounts/{username}/websites/{domain}/wordpress/plugins/deploy',
846
+ theme: '/api/hosting/v1/accounts/{username}/websites/{domain}/wordpress/themes/deploy',
847
+ };
848
+
849
+ async hosting_deploy_callWordpress(kind, params) {
850
+ return this.executeApiCall({ name: 'wordpress_' + kind, method: 'POST', path: this.hosting_deploy_wordpressEndpoints[kind] }, params);
851
+ }
852
+
853
+ hosting_deploy_randomSuffix() {
854
+ return Math.random().toString(36).slice(2, 10).padEnd(8, '0');
855
+ }
856
+
857
+ // A directory is zipped into a temp dir (removed by the caller); an archive is used as is.
858
+ async hosting_deploy_prepareArchive(sourcePath, exclude) {
859
+ const resolved = path.resolve(sourcePath);
860
+ if (fs.statSync(resolved).isFile()) {
861
+ const name = path.basename(resolved).toLowerCase();
862
+ const formats = ['.zip', '.tar', '.tar.gz', '.tgz', '.7z', '.gz', '.gzip'];
863
+ if (!formats.some((extension) => name.endsWith(extension))) {
864
+ throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
737
865
  }
866
+ return { archivePath: resolved, skipped: [] };
867
+ }
738
868
 
739
- this.log('info', `Successfully triggered import for website ${websiteUid}`);
740
- return response.data;
869
+ const { files, skipped } = this.hosting_deploy_listFiles(resolved, exclude);
870
+ if (files.length === 0) {
871
+ throw new Error('No files to deploy in ' + resolved);
872
+ }
873
+ let baseName = 'site';
874
+ try {
875
+ baseName = this.hosting_deploy_slug(resolved);
876
+ } catch (error) {
877
+ // an unusable directory name only changes the archive's file name
878
+ }
879
+ // A file name made of dots and backslashes would be written into the zip as it is, where an extractor
880
+ // that treats backslashes as separators reads it as a path outside the site.
881
+ for (const file of files) {
882
+ this.assertSafeUploadPath(file);
883
+ }
884
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hostinger-deploy-'));
885
+ const archivePath = path.join(tempDir, baseName + '_' + this.hosting_deploy_timestamp() + '.zip');
886
+ this.log('info', 'Packing ' + files.length + ' files from ' + resolved);
887
+ await this.hosting_deploy_createZip(resolved, files, archivePath);
888
+ return { archivePath, tempDir, skipped };
889
+ }
741
890
 
891
+ async hosting_deploy_upload(username, domain, files) {
892
+ // Check every name first, so a bad one fails the deploy before anything is uploaded.
893
+ for (const file of files) {
894
+ this.assertSafeUploadPath(file.relativePath);
895
+ }
896
+ let credentials;
897
+ try {
898
+ credentials = await this.fetchUploadCredentials(username, domain);
742
899
  } catch (error) {
743
- const errorMessage = error instanceof Error ? error.message : String(error);
744
- this.log('error', `Failed to trigger import: ${errorMessage}`);
900
+ throw new Error('Failed to fetch upload credentials: ' + (error instanceof Error ? error.message : String(error)));
901
+ }
902
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = credentials;
903
+ if (!uploadUrl || !authToken || !authRestToken) {
904
+ throw new Error('Invalid upload credentials received from API');
905
+ }
745
906
 
746
- if (axios.isAxiosError(error)) {
747
- const responseData = error.response?.data;
748
- const responseStatus = error.response?.status;
749
- this.log('error', 'API Error Details:', {
750
- status: responseStatus,
751
- data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
907
+ for (const file of files) {
908
+ try {
909
+ await this.uploadFile(file.absolutePath, file.relativePath, uploadUrl, authRestToken, authToken);
910
+ } catch (error) {
911
+ throw new Error('Failed to upload ' + file.relativePath + ': ' + (error instanceof Error ? error.message : String(error)));
912
+ }
913
+ }
914
+ }
915
+
916
+ async hosting_deploy_wordpressExtension(username, domain, type, params) {
917
+ const isPlugin = type === 'wordpress-plugin';
918
+ const directory = path.resolve(params.path);
919
+ const slug = params.slug !== undefined ? params.slug : this.hosting_deploy_slug(directory);
920
+ const { files, skipped } = this.hosting_deploy_listFiles(directory);
921
+ if (files.length === 0) {
922
+ throw new Error('No files found in directory: ' + directory);
923
+ }
924
+
925
+ // The files land in a fresh directory; the deploy call then installs from it.
926
+ const uploadDirName = slug + '-' + this.hosting_deploy_randomSuffix();
927
+ const remoteRoot = (isPlugin ? 'wp-content/plugins/' : 'wp-content/themes/') + uploadDirName;
928
+ await this.hosting_deploy_upload(username, domain, files.map((relativePath) => ({
929
+ absolutePath: path.join(directory, relativePath),
930
+ relativePath: remoteRoot + '/' + relativePath,
931
+ })));
932
+
933
+ if (isPlugin) {
934
+ await this.hosting_deploy_callWordpress('plugin', { username, domain, slug, plugin_path: uploadDirName });
935
+ } else {
936
+ await this.hosting_deploy_callWordpress('theme', { username, domain, slug, theme_path: uploadDirName, is_activated: params.activate === true });
937
+ }
938
+
939
+ return this.hosting_deploy_withSkipped({
940
+ type,
941
+ domain,
942
+ source: 'path',
943
+ status: 'deployed',
944
+ slug,
945
+ files: files.length,
946
+ ...(isPlugin ? {} : { activated: params.activate === true }),
947
+ }, skipped);
948
+ }
949
+
950
+ async hosting_deploy_fromPath(username, domain, type, params) {
951
+ if (type === 'wordpress-plugin' || type === 'wordpress-theme') {
952
+ return this.hosting_deploy_wordpressExtension(username, domain, type, params);
953
+ }
954
+
955
+ const uploads = [];
956
+ let databaseDump;
957
+ if (type === 'wordpress-site') {
958
+ databaseDump = path.resolve(params.databaseDump);
959
+ if (!databaseDump.toLowerCase().endsWith('.sql')) {
960
+ throw new Error('databaseDump must be a .sql file');
961
+ }
962
+ if (!fs.existsSync(databaseDump)) {
963
+ throw new Error('Database dump not found: ' + databaseDump);
964
+ }
965
+ }
966
+
967
+ const prepared = await this.hosting_deploy_prepareArchive(params.path, databaseDump);
968
+ try {
969
+ const archiveName = path.basename(prepared.archivePath);
970
+ uploads.push({ absolutePath: prepared.archivePath, relativePath: archiveName });
971
+ if (databaseDump !== undefined) {
972
+ uploads.push({ absolutePath: databaseDump, relativePath: path.basename(databaseDump) });
973
+ }
974
+ await this.hosting_deploy_upload(username, domain, uploads);
975
+
976
+ if (type === 'static') {
977
+ const result = await this.callOperation('hosting_websites_deploy-static-site-archive', { username, domain, archive_path: archiveName });
978
+ return this.hosting_deploy_withSkipped({ type, domain, source: 'path', status: 'deployed', message: result?.message }, prepared.skipped);
979
+ }
980
+
981
+ if (type === 'wordpress-site') {
982
+ const result = await this.hosting_deploy_callWordpress('import', {
983
+ username, domain, archive_path: archiveName, sql_path: path.basename(databaseDump),
752
984
  });
985
+ return this.hosting_deploy_withSkipped({
986
+ type, domain, source: 'path', status: 'import_started', message: result?.message,
987
+ next: 'Files are extracted on the server and the site is available in a few minutes.',
988
+ }, prepared.skipped);
753
989
  }
754
990
 
755
- throw error;
991
+ // nodejs: let the server detect the settings from the archive, then apply any overrides.
992
+ let detected = null;
993
+ let detectionError = '';
994
+ try {
995
+ detected = await this.callOperation('hosting_nodejs_build-settings-from-archive', { username, domain, archive_path: archiveName });
996
+ } catch (error) {
997
+ detectionError = error instanceof Error ? error.message : String(error);
998
+ this.log('warning', 'Build settings detection failed: ' + detectionError);
999
+ }
1000
+ let settings;
1001
+ try {
1002
+ settings = this.hosting_deploy_nodeSettings(detected, params, false);
1003
+ } catch (error) {
1004
+ const message = error instanceof Error ? error.message : String(error);
1005
+ throw new Error(detectionError ? message + ' (automatic detection failed: ' + detectionError + ')' : message);
1006
+ }
1007
+ const build = await this.callOperation('hosting_nodejs_start-build', {
1008
+ username, domain, ...settings, source_type: 'archive', source_options: { archive_path: archiveName },
1009
+ });
1010
+ return this.hosting_deploy_withSkipped(this.hosting_deploy_buildStarted(type, domain, 'path', build), prepared.skipped);
1011
+ } finally {
1012
+ if (prepared.tempDir) {
1013
+ fs.rmSync(prepared.tempDir, { recursive: true, force: true });
1014
+ }
756
1015
  }
757
1016
  }
758
1017
 
759
- async handlePhpAppDeploy(params) {
760
- const { domain, archivePath, removeArchive = true } = params;
1018
+ hosting_deploy_buildStarted(type, domain, source, build) {
1019
+ return {
1020
+ type,
1021
+ domain,
1022
+ source,
1023
+ status: 'build_started',
1024
+ buildUuid: build?.uuid,
1025
+ state: build?.state,
1026
+ next: 'The build runs on the server. Call hosting_get-deployment with this domain until the build state is completed or failed; a failed build comes back with its logs and analysis.',
1027
+ };
1028
+ }
761
1029
 
762
- this.agencyHosting_validateRequiredParams(params);
763
- this.agencyHosting_validateArchiveFile(archivePath);
1030
+ // The Git installation that can reach owner/name (GitLab owners may contain slashes).
1031
+ async hosting_deploy_findRepository(fullName) {
1032
+ if (!fullName.includes('/')) {
1033
+ throw new Error('git.repository must be given as owner/name');
1034
+ }
1035
+ const installations = await this.callOperation('hosting_git_list-installations', { status: 'active' });
1036
+ if (!Array.isArray(installations) || installations.length === 0) {
1037
+ throw new Error('No active Git installation. Connect GitHub or GitLab once in hPanel (Websites, Manage, Advanced, Git), then try again.');
1038
+ }
1039
+ for (const installation of installations) {
1040
+ const repositories = await this.callOperation('hosting_git_list-installation-repositories', { uuid: installation.uuid });
1041
+ const repo = (Array.isArray(repositories) ? repositories : []).find((candidate) => String(candidate.full_name).toLowerCase() === fullName.toLowerCase());
1042
+ if (repo) {
1043
+ return { installationUuid: installation.uuid, owner: repo.owner, repository: repo.name };
1044
+ }
1045
+ }
1046
+ throw new Error('Repository ' + fullName + ' was not found in your ' + installations.length + ' active Git installation(s). Check the name, or give the Git provider access to it in hPanel (Websites, Manage, Advanced, Git).');
1047
+ }
764
1048
 
765
- this.log('info', `Resolving website UID from domain: ${domain}`);
766
- const websiteUid = await this.agencyHosting_resolveWebsiteUid(domain);
1049
+ // What a Git deploy is. There are no local files to look at, so guessing is dangerous: a Node.js
1050
+ // repository deployed as a static site would publish its source and switch on push-to-deploy. The
1051
+ // type given, the site already running Node.js, or Node.js settings in the request are clear
1052
+ // intent; anything else has to say what it is.
1053
+ hosting_deploy_gitType(website, params) {
1054
+ if (params.type !== undefined) {
1055
+ return params.type;
1056
+ }
1057
+ if (website.website_type === 'nodejs' || this.hosting_deploy_hasNodeOverrides(params)) {
1058
+ return 'nodejs';
1059
+ }
1060
+ throw new Error("A Git deploy needs type ('static' or 'nodejs'): this site is not a Node.js site yet, and a repository's kind cannot be told without its files.");
1061
+ }
767
1062
 
768
- this.log('info', `Starting archive upload for ${domain}`);
1063
+ async hosting_deploy_fromGit(website, domain, git, params) {
1064
+ const username = website.username;
1065
+ const type = this.hosting_deploy_gitType(website, params);
1066
+ if (type !== 'nodejs' && type !== 'static') {
1067
+ throw new Error('A Git deploy supports type static or nodejs, not ' + type);
1068
+ }
1069
+ const directory = this.hosting_deploy_gitDirectory(git);
1070
+ if (type === 'nodejs' && directory !== '') {
1071
+ throw new Error('git.directory is for static deploys: a Node.js build has no target folder');
1072
+ }
1073
+ const repo = await this.hosting_deploy_findRepository(git.repository);
769
1074
 
770
- let uploadCredentials;
1075
+ if (type === 'static') {
1076
+ // Saving the settings deploys the branch now and again on every push.
1077
+ const result = await this.callOperation('hosting_git_update-auto-deployment-settings', {
1078
+ username, domain, installation_uuid: repo.installationUuid, owner: repo.owner, repository: repo.repository, branch: git.branch, is_enabled: true,
1079
+ ...(directory !== '' ? { directory } : {}),
1080
+ });
1081
+ return { type, domain, source: 'git', status: 'deployed', autoDeploy: true, message: result?.message };
1082
+ }
1083
+
1084
+ // Nothing is uploaded, so there is no archive to detect from: use the settings stored for the site.
1085
+ let stored = null;
771
1086
  try {
772
- uploadCredentials = await this.agencyHosting_fetchUploadCredentials(websiteUid);
1087
+ stored = await this.callOperation('hosting_nodejs_build-settings', { username, domain });
773
1088
  } catch (error) {
774
- const errorMessage = error instanceof Error ? error.message : String(error);
775
- throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
1089
+ if (!(error instanceof ApiExecutionError && error.status === 404)) {
1090
+ throw error;
1091
+ }
1092
+ }
1093
+ const settings = this.hosting_deploy_nodeSettings(stored, params, true);
1094
+ if (this.hosting_deploy_hasNodeOverrides(params)) {
1095
+ // Later pushes build with the stored settings, not with this request: keep what was given.
1096
+ await this.callOperation('hosting_nodejs_update-build-settings', { username, domain, ...settings });
776
1097
  }
1098
+ const build = await this.callOperation('hosting_nodejs_start-build', {
1099
+ username, domain, ...settings, source_type: 'git',
1100
+ source_options: { owner: repo.owner, repository: repo.repository, branch: git.branch, installation_uuid: repo.installationUuid },
1101
+ });
1102
+ return this.hosting_deploy_buildStarted(type, domain, 'git', build);
1103
+ }
777
1104
 
778
- const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
1105
+ async handleHostingDeploy(params) {
1106
+ this.hosting_deploy_validate(params);
1107
+ const { domain, git } = params;
779
1108
 
780
- if (!uploadUrl || !authToken || !authRestToken) {
781
- throw new Error('Invalid upload credentials received from API');
1109
+ if (git === undefined) {
1110
+ // Local checks first, so a bad path fails before any request is made.
1111
+ const type = this.hosting_deploy_resolveType(params);
1112
+ const website = await this.findWebsite(domain);
1113
+ return this.hosting_deploy_fromPath(website.username, domain, type, params);
782
1114
  }
1115
+ const website = await this.findWebsite(domain);
1116
+ return this.hosting_deploy_fromGit(website, domain, git, params);
1117
+ }
783
1118
 
784
- const archiveBasename = path.basename(archivePath);
785
- let uploadResult;
786
- try {
787
- uploadResult = await this.agencyHosting_uploadFile(
788
- archivePath,
789
- `.h5g/${archiveBasename}`,
790
- uploadUrl,
791
- authRestToken,
792
- authToken
793
- );
1119
+ // The failure analysis re-runs on every call and is limited to 5 a minute, so keep one request per
1120
+ // build, stored as soon as it starts so that polls at the same moment share it.
1121
+ hosting_getDeployment_analyses = new Map ();
794
1122
 
795
- this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
1123
+ // A failed analysis (429, outage) is remembered for a minute, so polling does not retry the
1124
+ // rate-limited endpoint on every call; after that it is tried again.
1125
+ hosting_getDeployment_failures = new Map ();
1126
+
1127
+ // Settings that do not exist yet answer 404; that is "none", not a failure.
1128
+ async hosting_getDeployment_orNull(request) {
1129
+ try {
1130
+ return await request;
796
1131
  } catch (error) {
797
- const errorMessage = error instanceof Error ? error.message : String(error);
798
- throw new Error(`Failed to upload archive: ${errorMessage}`);
1132
+ if (error instanceof ApiExecutionError && error.status === 404) {
1133
+ return null;
1134
+ }
1135
+ throw error;
799
1136
  }
1137
+ }
800
1138
 
801
- let deployResult;
1139
+ async hosting_getDeployment_analysis(username, domain, uuid) {
1140
+ const key = username + '/' + domain + '/' + uuid;
1141
+ const failure = this.hosting_getDeployment_failures.get(key);
1142
+ if (failure && Date.now() - failure.at < 60000) {
1143
+ return { error: failure.error };
1144
+ }
1145
+ if (!this.hosting_getDeployment_analyses.has(key)) {
1146
+ this.hosting_getDeployment_analyses.set(key, this.callOperation('hosting_nodejs_analyse-failed-build', { username, domain, uuid }));
1147
+ }
802
1148
  try {
803
- this.log('info', `Triggering import for ${domain}`);
804
- deployResult = await this.agencyHosting_deployPhpApp_triggerImportArchive(websiteUid, archiveBasename);
1149
+ const analysis = await this.hosting_getDeployment_analyses.get(key);
1150
+ this.hosting_getDeployment_failures.delete(key);
1151
+ return analysis;
805
1152
  } catch (error) {
806
- const errorMessage = error instanceof Error ? error.message : String(error);
807
- this.log('error', `Failed to trigger import: ${errorMessage}`);
808
- const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
1153
+ // Rate limited or unavailable: report it, but do not lose the build and its logs.
1154
+ this.hosting_getDeployment_analyses.delete(key);
1155
+ const message = this.formatToolError(error);
1156
+ this.hosting_getDeployment_failures.set(key, { error: message, at: Date.now() });
1157
+ return { error: message };
1158
+ }
1159
+ }
809
1160
 
810
- return {
811
- upload: {
812
- status: 'success',
813
- data: {
814
- filename: uploadResult.filename
815
- }
816
- },
817
- deploy: {
818
- status: 'error',
819
- error: errorMessage
820
- },
821
- removeArchive: {
822
- status: archiveRemoved ? 'success' : 'skipped'
823
- }
824
- };
1161
+ async handleHostingGetDeployment(params) {
1162
+ const { domain, buildUuid, fromLine } = params;
1163
+
1164
+ if (!domain || typeof domain !== 'string') {
1165
+ throw new Error('domain is required and must be a string');
1166
+ }
1167
+ if (buildUuid !== undefined && (typeof buildUuid !== 'string' || buildUuid === '')) {
1168
+ throw new Error('buildUuid must be a non-empty string when provided');
1169
+ }
1170
+ if (fromLine !== undefined && (typeof fromLine !== 'number' || !Number.isInteger(fromLine) || fromLine < 0)) {
1171
+ throw new Error('fromLine must be a non-negative integer when provided');
825
1172
  }
826
1173
 
827
- const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
1174
+ const { username } = await this.findWebsite(domain);
1175
+ const target = { username, domain };
828
1176
 
829
- return {
830
- upload: {
831
- status: 'success',
832
- data: {
833
- filename: uploadResult.filename
834
- }
835
- },
836
- deploy: {
837
- status: 'success',
838
- data: deployResult
839
- },
840
- removeArchive: {
841
- status: archiveRemoved ? 'success' : 'skipped'
842
- }
843
- };
844
- }
845
-
846
- normalizePath(pathString) {
847
- return pathString.replace(/\\/g, '/');
848
- }
1177
+ const gitSettings = await this.callOperation('hosting_git_auto-deployment-settings', target);
1178
+ const gitAutoDeployment = gitSettings && gitSettings.installation_uuid ? gitSettings : null;
1179
+ const buildSettings = await this.hosting_getDeployment_orNull(this.callOperation('hosting_nodejs_build-settings', target));
849
1180
 
850
- async resolveUsername(domain) {
851
- const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
852
- const url = new URL(`api/hosting/v1/websites?domain=${encodeURIComponent(domain)}`, baseUrl).toString();
853
-
854
- try {
855
- const bearerToken = await this.getAuthToken();
856
-
857
- const config = {
858
- method: 'get',
859
- url,
860
- headers: {
861
- ...this.headers,
862
- 'Authorization': `Bearer ${bearerToken}`
863
- },
864
- timeout: 60000, // 60s
865
- validateStatus: function (status) {
866
- return status < 500;
867
- }
868
- };
869
-
870
- const response = await axios(config);
871
-
872
- if (response.status !== 200) {
873
- throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
874
- }
875
-
876
- const websites = response.data?.data;
877
- if (!websites || websites.length === 0) {
878
- throw new Error(`No website found for domain: ${domain}`);
879
- }
880
-
881
- const username = websites[0].username;
882
- if (!username) {
883
- throw new Error(`Username not found in website data for domain: ${domain}`);
884
- }
885
-
886
- this.log('info', `Resolved username: ${username} for domain: ${domain}`);
887
- return username;
888
-
889
- } catch (error) {
890
- const errorMessage = error instanceof Error ? error.message : String(error);
891
- this.log('error', `Failed to resolve username for domain ${domain}: ${errorMessage}`);
892
-
893
- if (axios.isAxiosError(error)) {
894
- const responseData = error.response?.data;
895
- const responseStatus = error.response?.status;
896
- this.log('error', 'API Error Details:', {
897
- status: responseStatus,
898
- data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
899
- });
900
- }
901
-
902
- throw error;
1181
+ let build = null;
1182
+ if (buildUuid !== undefined) {
1183
+ build = await this.callOperation('hosting_nodejs_build', { ...target, uuid: buildUuid });
1184
+ } else {
1185
+ const builds = await this.hosting_getDeployment_orNull(this.callOperation('hosting_nodejs_list-builds', { ...target, per_page: 1 }));
1186
+ build = builds && Array.isArray(builds.data) && builds.data.length > 0 ? builds.data[0] : null;
903
1187
  }
904
- }
905
1188
 
906
- async fetchUploadCredentials(username, domain) {
907
- const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
908
- const url = new URL('api/hosting/v1/files/upload-urls', baseUrl).toString();
909
-
910
- try {
911
- const bearerToken = await this.getAuthToken();
912
-
913
- const config = {
914
- method: 'post',
915
- url,
916
- headers: {
917
- ...this.headers,
918
- 'Authorization': `Bearer ${bearerToken}`,
919
- 'Content-Type': 'application/json'
920
- },
921
- data: {
922
- username,
923
- domain
924
- },
925
- timeout: 60000, // 60s
926
- validateStatus: function (status) {
927
- return status < 500;
928
- }
1189
+ if (build === null) {
1190
+ return {
1191
+ domain,
1192
+ build: null,
1193
+ buildSettings,
1194
+ gitAutoDeployment,
1195
+ note: 'No Node.js builds found for this website. Static, PHP, plugin and theme deploys finish before hosting_deploy returns, and a WordPress site import is ready a few minutes after it starts; a Node.js build appears here once one has been started.',
929
1196
  };
930
-
931
- const response = await axios(config);
932
-
933
- if (response.status !== 200) {
934
- throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
935
- }
936
-
937
- return response.data;
938
-
939
- } catch (error) {
940
- const errorMessage = error instanceof Error ? error.message : String(error);
941
- this.log('error', `Failed to fetch upload credentials: ${errorMessage}`);
942
-
943
- if (axios.isAxiosError(error)) {
944
- const responseData = error.response?.data;
945
- const responseStatus = error.response?.status;
946
- this.log('error', 'API Error Details:', {
947
- status: responseStatus,
948
- data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
949
- });
950
- }
951
-
952
- throw error;
953
1197
  }
954
- }
955
-
956
- async uploadFile(filePath, relativePath, uploadUrl, authRestToken, authToken) {
957
- return new Promise(async (resolve, reject) => {
958
- try {
959
- const stats = fs.statSync(filePath);
960
- const fileStream = fs.createReadStream(filePath);
961
-
962
- const cleanUploadUrl = uploadUrl.replace(new RegExp('/$'), '');
963
- const normalizedPath = this.normalizePath(relativePath);
964
- const uploadUrlWithFile = `${cleanUploadUrl}/${normalizedPath}?override=true`;
965
-
966
- const requestHeaders = {
967
- 'X-Auth': authToken,
968
- 'X-Auth-Rest': authRestToken,
969
- 'upload-length': stats.size.toString(),
970
- 'upload-offset': '0'
971
- };
972
-
973
- try {
974
- this.log('debug', `Making pre-upload POST request to ${uploadUrlWithFile}`);
975
- await axios.post(uploadUrlWithFile, '', {
976
- headers: requestHeaders,
977
- timeout: 60000, // 60s
978
- validateStatus: function (status) {
979
- return status == 201;
980
- }
981
- });
982
- } catch (error) {
983
- const errorMessage = error instanceof Error ? error.message : String(error);
984
-
985
- if (axios.isAxiosError(error)) {
986
- const responseData = error.response?.data;
987
- const responseStatus = error.response?.status;
988
- const responseHeaders = error.response?.headers;
989
- const responseText = typeof responseData === 'object' ? JSON.stringify(responseData) : responseData;
990
-
991
- this.log('error', 'Pre-upload POST request failed - Full Response Details:', {
992
- status: responseStatus,
993
- headers: responseHeaders,
994
- data: responseText,
995
- message: errorMessage
996
- });
997
- reject(new Error(`Pre-upload request failed: ${errorMessage}`));
998
- return;
999
- } else {
1000
- this.log('error', `Pre-upload POST request failed: ${errorMessage}`);
1001
- reject(new Error(`Pre-upload request failed: ${errorMessage}`));
1002
- return;
1003
- }
1004
- }
1005
-
1006
- const upload = new tus.Upload(fileStream, {
1007
- uploadUrl: uploadUrlWithFile,
1008
- retryDelays: [1000, 2000, 4000, 8000, 16000, 20000],
1009
- uploadDataDuringCreation: false,
1010
- parallelUploads: 1,
1011
- chunkSize: 10485760,
1012
- headers: requestHeaders,
1013
- removeFingerprintOnSuccess: true,
1014
- uploadSize: stats.size,
1015
- metadata: {
1016
- filename: path.basename(relativePath)
1017
- },
1018
- onError: (error) => {
1019
- this.log('error', `TUS upload error for ${relativePath}`, { error: error.message });
1020
- reject(new Error(`Upload failed: ${error.message}`));
1021
- },
1022
- onSuccess: () => {
1023
- this.log('info', `TUS upload completed for ${relativePath}`, { url: upload.url });
1024
- resolve({
1025
- url: upload.url,
1026
- filename: relativePath
1027
- });
1028
- }
1029
- });
1030
1198
 
1031
- upload.start();
1199
+ const logs = await this.callOperation('hosting_nodejs_build-logs', { ...target, uuid: build.uuid, from_line: fromLine !== undefined ? fromLine : 0 });
1032
1200
 
1033
- } catch (error) {
1034
- const errorMessage = error instanceof Error ? error.message : String(error);
1035
- this.log('error', `Error preparing upload for ${filePath}`, { error: errorMessage });
1036
- reject(new Error(`Failed to prepare upload: ${errorMessage}`));
1037
- }
1038
- });
1201
+ return {
1202
+ domain,
1203
+ build,
1204
+ logs,
1205
+ ...(build.state === 'failed' ? { analysis: await this.hosting_getDeployment_analysis(username, domain, build.uuid) } : {}),
1206
+ buildSettings,
1207
+ gitAutoDeployment,
1208
+ };
1039
1209
  }
1040
1210
 
1041
1211
  hosting_importWordpressWebsite_validateArchiveFormat(filePath) {
@@ -1123,35 +1293,491 @@ class MCPServer {
1123
1293
  throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1124
1294
  }
1125
1295
 
1126
- const { is_empty } = response.data;
1296
+ const { is_empty } = response.data;
1297
+
1298
+ if (!is_empty) {
1299
+ throw new Error('Website is not empty. WordPress import can only be performed on empty sites. Please visit hPanel (https://hpanel.hostinger.com) and remove all existing files from the website before attempting to import.');
1300
+ }
1301
+
1302
+ this.log('info', `Website ${domain} is empty, proceeding with import`);
1303
+ return true;
1304
+
1305
+ } catch (error) {
1306
+ const errorMessage = error instanceof Error ? error.message : String(error);
1307
+ this.log('error', `Failed to check if website is empty: ${errorMessage}`);
1308
+
1309
+ if (axios.isAxiosError(error)) {
1310
+ const responseData = error.response?.data;
1311
+ const responseStatus = error.response?.status;
1312
+ this.log('error', 'API Error Details:', {
1313
+ status: responseStatus,
1314
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1315
+ });
1316
+ }
1317
+
1318
+ throw error;
1319
+ }
1320
+ }
1321
+
1322
+ async hosting_importWordpressWebsite_extractFiles(username, domain, archivePath, databaseDump) {
1323
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1324
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/import`, baseUrl).toString();
1325
+
1326
+ try {
1327
+ const bearerToken = await this.getAuthToken();
1328
+
1329
+ const config = {
1330
+ method: 'post',
1331
+ url,
1332
+ headers: {
1333
+ ...this.headers,
1334
+ 'Authorization': `Bearer ${bearerToken}`,
1335
+ 'Content-Type': 'application/json'
1336
+ },
1337
+ data: {
1338
+ archive_path: path.basename(archivePath),
1339
+ sql_path: path.basename(databaseDump)
1340
+ },
1341
+ timeout: 60000, // 60s
1342
+ validateStatus: function (status) {
1343
+ return status < 500;
1344
+ }
1345
+ };
1346
+
1347
+ const response = await axios(config);
1348
+
1349
+ this.log('info', `Successfully triggered file extraction for ${domain}`);
1350
+ return true;
1351
+
1352
+ } catch (error) {
1353
+ const errorMessage = error instanceof Error ? error.message : String(error);
1354
+ this.log('error', `Failed to trigger file extraction: ${errorMessage}`);
1355
+
1356
+ if (axios.isAxiosError(error)) {
1357
+ const responseData = error.response?.data;
1358
+ const responseStatus = error.response?.status;
1359
+ this.log('error', 'API Error Details:', {
1360
+ status: responseStatus,
1361
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1362
+ });
1363
+ }
1364
+
1365
+ throw error;
1366
+ }
1367
+ }
1368
+
1369
+ async handleWordpressWebsiteImport(params) {
1370
+ const { domain, archivePath, databaseDump } = params;
1371
+
1372
+ this.hosting_importWordpressWebsite_validateRequiredParams(params);
1373
+ this.hosting_importWordpressWebsite_validateArchiveFile(archivePath);
1374
+ this.hosting_importWordpressWebsite_validateDatabaseFile(databaseDump);
1375
+
1376
+ // Auto-resolve username from domain
1377
+ this.log('info', `Resolving username from domain: ${domain}`);
1378
+ const username = await this.resolveUsername(domain);
1379
+
1380
+ await this.hosting_importWordpressWebsite_checkWebsiteIsEmpty(username, domain);
1381
+
1382
+ const filesToUpload = [{
1383
+ absolutePath: archivePath,
1384
+ relativePath: path.basename(archivePath),
1385
+ type: 'archive'
1386
+ }, {
1387
+ absolutePath: databaseDump,
1388
+ relativePath: path.basename(databaseDump),
1389
+ type: 'database'
1390
+ }];
1391
+
1392
+ let uploadCredentials;
1393
+ try {
1394
+ uploadCredentials = await this.fetchUploadCredentials(username, domain);
1395
+ } catch (error) {
1396
+ const errorMessage = error instanceof Error ? error.message : String(error);
1397
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
1398
+ }
1399
+
1400
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
1401
+
1402
+ if (!uploadUrl || !authToken || !authRestToken) {
1403
+ throw new Error('Invalid upload credentials received from API');
1404
+ }
1405
+
1406
+ this.log('info', `Starting website archive import to ${uploadUrl}`);
1407
+
1408
+ const results = [];
1409
+ let successCount = 0;
1410
+ let failureCount = 0;
1411
+
1412
+ for (const fileInfo of filesToUpload) {
1413
+ try {
1414
+ this.log('info', `Uploading ${fileInfo.type}: ${fileInfo.absolutePath}`);
1415
+
1416
+ const stats = fs.statSync(fileInfo.absolutePath);
1417
+ const uploadResult = await this.uploadFile(
1418
+ fileInfo.absolutePath,
1419
+ fileInfo.relativePath,
1420
+ uploadUrl,
1421
+ authRestToken,
1422
+ authToken
1423
+ );
1424
+
1425
+ results.push({
1426
+ file: fileInfo.absolutePath,
1427
+ remotePath: fileInfo.relativePath,
1428
+ type: fileInfo.type,
1429
+ status: 'success',
1430
+ uploadUrl: uploadResult.url,
1431
+ size: stats.size
1432
+ });
1433
+
1434
+ successCount++;
1435
+ this.log('info', `Successfully uploaded ${fileInfo.type}: ${fileInfo.relativePath}`);
1436
+
1437
+ } catch (error) {
1438
+ const errorMessage = error instanceof Error ? error.message : String(error);
1439
+ results.push({
1440
+ file: fileInfo.absolutePath,
1441
+ remotePath: fileInfo.relativePath,
1442
+ type: fileInfo.type,
1443
+ status: 'error',
1444
+ error: errorMessage
1445
+ });
1446
+
1447
+ failureCount++;
1448
+ this.log('error', `Failed to upload ${fileInfo.type} ${fileInfo.absolutePath}: ${errorMessage}`);
1449
+ }
1450
+ }
1451
+
1452
+ const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
1453
+
1454
+ if (failureCount === 0) {
1455
+ try {
1456
+ this.log('info', 'All files uploaded successfully, triggering extraction...');
1457
+ await this.hosting_importWordpressWebsite_extractFiles(username, domain, archivePath, databaseDump);
1458
+ } catch (error) {
1459
+ const errorMessage = error instanceof Error ? error.message : String(error);
1460
+ this.log('error', `File extraction failed: ${errorMessage}`);
1461
+ return {
1462
+ status: 'partial',
1463
+ summary: {
1464
+ total: filesToUpload.length,
1465
+ successful: successCount,
1466
+ failed: failureCount
1467
+ },
1468
+ results,
1469
+ extractionError: errorMessage
1470
+ };
1471
+ }
1472
+ }
1473
+
1474
+ return {
1475
+ status: overallStatus,
1476
+ summary: {
1477
+ total: filesToUpload.length,
1478
+ successful: successCount,
1479
+ failed: failureCount
1480
+ },
1481
+ results
1482
+ };
1483
+ }
1484
+
1485
+ hosting_deployWordpressPlugin_generateRandomString(length) {
1486
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1487
+ let result = '';
1488
+ for (let i = 0; i < length; i++) {
1489
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
1490
+ }
1491
+ return result;
1492
+ }
1493
+
1494
+ hosting_deployWordpressPlugin_validateRequiredParams(params) {
1495
+ const { domain, slug, pluginPath } = params;
1496
+
1497
+ if (!domain || typeof domain !== 'string') {
1498
+ throw new Error('domain is required and must be a string');
1499
+ }
1500
+
1501
+ if (!slug || typeof slug !== 'string') {
1502
+ throw new Error('slug is required and must be a string');
1503
+ }
1504
+
1505
+ if (!pluginPath || typeof pluginPath !== 'string') {
1506
+ throw new Error('pluginPath is required and must be a string');
1507
+ }
1508
+ }
1509
+
1510
+ hosting_deployWordpressPlugin_validatePluginDirectory(pluginPath) {
1511
+ if (!fs.existsSync(pluginPath)) {
1512
+ throw new Error(`Plugin directory not found: ${pluginPath}`);
1513
+ }
1514
+
1515
+ const pluginStats = fs.statSync(pluginPath);
1516
+ if (!pluginStats.isDirectory()) {
1517
+ throw new Error(`Plugin path is not a directory: ${pluginPath}`);
1518
+ }
1519
+ }
1520
+
1521
+ hosting_deployWordpressPlugin_scanDirectory(dirPath, basePath = dirPath) {
1522
+ const files = [];
1523
+
1524
+ const scanDir = (currentPath) => {
1525
+ const items = fs.readdirSync(currentPath);
1526
+
1527
+ for (const item of items) {
1528
+ const itemPath = path.join(currentPath, item);
1529
+ const stats = fs.statSync(itemPath);
1530
+
1531
+ if (stats.isDirectory()) {
1532
+ scanDir(itemPath);
1533
+ } else if (stats.isFile()) {
1534
+ const relativePath = path.relative(basePath, itemPath);
1535
+ files.push({
1536
+ absolutePath: itemPath,
1537
+ relativePath: relativePath
1538
+ });
1539
+ }
1540
+ }
1541
+ };
1542
+
1543
+ scanDir(dirPath);
1544
+ return files;
1545
+ }
1546
+
1547
+ async hosting_deployWordpressPlugin_deployPlugin(username, domain, slug, pluginPath) {
1548
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1549
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/plugins/deploy`, baseUrl).toString();
1550
+
1551
+ try {
1552
+ const bearerToken = await this.getAuthToken();
1553
+
1554
+ const config = {
1555
+ method: 'post',
1556
+ url,
1557
+ headers: {
1558
+ ...this.headers,
1559
+ 'Authorization': `Bearer ${bearerToken}`,
1560
+ 'Content-Type': 'application/json'
1561
+ },
1562
+ data: {
1563
+ slug,
1564
+ plugin_path: pluginPath
1565
+ },
1566
+ timeout: 60000, // 60s
1567
+ validateStatus: function (status) {
1568
+ return status < 500;
1569
+ }
1570
+ };
1571
+
1572
+ const response = await axios(config);
1573
+
1574
+ if (response.status !== 200) {
1575
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1576
+ }
1577
+
1578
+ this.log('info', `Successfully triggered plugin deployment for ${domain}`);
1579
+ return true;
1580
+
1581
+ } catch (error) {
1582
+ const errorMessage = error instanceof Error ? error.message : String(error);
1583
+ this.log('error', `Failed to trigger plugin deployment: ${errorMessage}`);
1584
+
1585
+ if (axios.isAxiosError(error)) {
1586
+ const responseData = error.response?.data;
1587
+ const responseStatus = error.response?.status;
1588
+ this.log('error', 'API Error Details:', {
1589
+ status: responseStatus,
1590
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1591
+ });
1592
+ }
1593
+
1594
+ throw error;
1595
+ }
1596
+ }
1597
+
1598
+ async handleWordpressPluginDeploy(params) {
1599
+ const { domain, slug, pluginPath } = params;
1600
+
1601
+ this.hosting_deployWordpressPlugin_validateRequiredParams(params);
1602
+ this.hosting_deployWordpressPlugin_validatePluginDirectory(pluginPath);
1603
+
1604
+ this.log('info', `Resolving username from domain: ${domain}`);
1605
+ const username = await this.resolveUsername(domain);
1606
+
1607
+ const randomSuffix = this.hosting_deployWordpressPlugin_generateRandomString(8);
1608
+ const uploadDirName = `${slug}-${randomSuffix}`;
1609
+
1610
+ this.log('info', `Scanning plugin directory: ${pluginPath}`);
1611
+ const pluginFiles = this.hosting_deployWordpressPlugin_scanDirectory(pluginPath);
1612
+
1613
+ if (pluginFiles.length === 0) {
1614
+ throw new Error(`No files found in plugin directory: ${pluginPath}`);
1615
+ }
1616
+
1617
+ this.log('info', `Found ${pluginFiles.length} files to upload`);
1618
+
1619
+ let uploadCredentials;
1620
+ try {
1621
+ uploadCredentials = await this.fetchUploadCredentials(username, domain);
1622
+ } catch (error) {
1623
+ const errorMessage = error instanceof Error ? error.message : String(error);
1624
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
1625
+ }
1626
+
1627
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
1628
+
1629
+ if (!uploadUrl || !authToken || !authRestToken) {
1630
+ throw new Error('Invalid upload credentials received from API');
1631
+ }
1632
+
1633
+ this.log('info', `Starting plugin file upload to ${uploadUrl}`);
1634
+
1635
+ const results = [];
1636
+ let successCount = 0;
1637
+ let failureCount = 0;
1638
+
1639
+ for (const fileInfo of pluginFiles) {
1640
+ try {
1641
+ const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
1642
+ const uploadPath = `wp-content/plugins/${uploadDirName}/${normalizedRelativePath}`;
1643
+ this.log('info', `Uploading: ${fileInfo.absolutePath} -> ${uploadPath}`);
1644
+
1645
+ const stats = fs.statSync(fileInfo.absolutePath);
1646
+ const uploadResult = await this.uploadFile(
1647
+ fileInfo.absolutePath,
1648
+ uploadPath,
1649
+ uploadUrl,
1650
+ authRestToken,
1651
+ authToken
1652
+ );
1653
+
1654
+ results.push({
1655
+ file: fileInfo.absolutePath,
1656
+ remotePath: uploadPath,
1657
+ status: 'success',
1658
+ uploadUrl: uploadResult.url,
1659
+ size: stats.size
1660
+ });
1661
+
1662
+ successCount++;
1663
+ this.log('info', `Successfully uploaded: ${uploadPath}`);
1664
+
1665
+ } catch (error) {
1666
+ const errorMessage = error instanceof Error ? error.message : String(error);
1667
+ const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
1668
+ const uploadPath = `wp-content/plugins/${uploadDirName}/${normalizedRelativePath}`;
1669
+
1670
+ results.push({
1671
+ file: fileInfo.absolutePath,
1672
+ remotePath: uploadPath,
1673
+ status: 'error',
1674
+ error: errorMessage
1675
+ });
1676
+
1677
+ failureCount++;
1678
+ this.log('error', `Failed to upload ${fileInfo.absolutePath}: ${errorMessage}`);
1679
+ }
1680
+ }
1681
+
1682
+ const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
1683
+
1684
+ if (failureCount === 0) {
1685
+ try {
1686
+ this.log('info', 'All files uploaded successfully, triggering plugin deployment...');
1687
+ await this.hosting_deployWordpressPlugin_deployPlugin(username, domain, slug, uploadDirName);
1688
+ } catch (error) {
1689
+ const errorMessage = error instanceof Error ? error.message : String(error);
1690
+ this.log('error', `Plugin deployment failed: ${errorMessage}`);
1691
+ return {
1692
+ status: 'partial',
1693
+ summary: {
1694
+ total: pluginFiles.length,
1695
+ successful: successCount,
1696
+ failed: failureCount
1697
+ },
1698
+ results,
1699
+ deploymentError: errorMessage
1700
+ };
1701
+ }
1702
+ }
1703
+
1704
+ return {
1705
+ status: overallStatus,
1706
+ summary: {
1707
+ total: pluginFiles.length,
1708
+ successful: successCount,
1709
+ failed: failureCount
1710
+ },
1711
+ results,
1712
+ uploadDirName
1713
+ };
1714
+ }
1715
+
1716
+ hosting_deployWordpressTheme_generateRandomString(length) {
1717
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1718
+ let result = '';
1719
+ for (let i = 0; i < length; i++) {
1720
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
1721
+ }
1722
+ return result;
1723
+ }
1724
+
1725
+ hosting_deployWordpressTheme_validateRequiredParams(params) {
1726
+ const { domain, slug, themePath } = params;
1727
+
1728
+ if (!domain || typeof domain !== 'string') {
1729
+ throw new Error('domain is required and must be a string');
1730
+ }
1731
+
1732
+ if (!slug || typeof slug !== 'string') {
1733
+ throw new Error('slug is required and must be a string');
1734
+ }
1127
1735
 
1128
- if (!is_empty) {
1129
- throw new Error('Website is not empty. WordPress import can only be performed on empty sites. Please visit hPanel (https://hpanel.hostinger.com) and remove all existing files from the website before attempting to import.');
1130
- }
1736
+ if (!themePath || typeof themePath !== 'string') {
1737
+ throw new Error('themePath is required and must be a string');
1738
+ }
1739
+ }
1131
1740
 
1132
- this.log('info', `Website ${domain} is empty, proceeding with import`);
1133
- return true;
1741
+ hosting_deployWordpressTheme_validateThemeDirectory(themePath) {
1742
+ if (!fs.existsSync(themePath)) {
1743
+ throw new Error(`Theme directory not found: ${themePath}`);
1744
+ }
1134
1745
 
1135
- } catch (error) {
1136
- const errorMessage = error instanceof Error ? error.message : String(error);
1137
- this.log('error', `Failed to check if website is empty: ${errorMessage}`);
1746
+ const themeStats = fs.statSync(themePath);
1747
+ if (!themeStats.isDirectory()) {
1748
+ throw new Error(`Theme path is not a directory: ${themePath}`);
1749
+ }
1750
+ }
1138
1751
 
1139
- if (axios.isAxiosError(error)) {
1140
- const responseData = error.response?.data;
1141
- const responseStatus = error.response?.status;
1142
- this.log('error', 'API Error Details:', {
1143
- status: responseStatus,
1144
- data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1145
- });
1752
+ hosting_deployWordpressTheme_scanDirectory(dirPath, basePath = dirPath) {
1753
+ const files = [];
1754
+
1755
+ const scanDir = (currentPath) => {
1756
+ const items = fs.readdirSync(currentPath);
1757
+
1758
+ for (const item of items) {
1759
+ const itemPath = path.join(currentPath, item);
1760
+ const stats = fs.statSync(itemPath);
1761
+
1762
+ if (stats.isDirectory()) {
1763
+ scanDir(itemPath);
1764
+ } else if (stats.isFile()) {
1765
+ const relativePath = path.relative(basePath, itemPath);
1766
+ files.push({
1767
+ absolutePath: itemPath,
1768
+ relativePath: relativePath
1769
+ });
1770
+ }
1146
1771
  }
1147
-
1148
- throw error;
1149
- }
1772
+ };
1773
+
1774
+ scanDir(dirPath);
1775
+ return files;
1150
1776
  }
1151
1777
 
1152
- async hosting_importWordpressWebsite_extractFiles(username, domain, archivePath, databaseDump) {
1778
+ async hosting_deployWordpressTheme_deployTheme(username, domain, slug, themePath, activate = false) {
1153
1779
  const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1154
- const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/import`, baseUrl).toString();
1780
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/themes/deploy`, baseUrl).toString();
1155
1781
 
1156
1782
  try {
1157
1783
  const bearerToken = await this.getAuthToken();
@@ -1165,8 +1791,9 @@ class MCPServer {
1165
1791
  'Content-Type': 'application/json'
1166
1792
  },
1167
1793
  data: {
1168
- archive_path: path.basename(archivePath),
1169
- sql_path: path.basename(databaseDump)
1794
+ slug,
1795
+ theme_path: themePath,
1796
+ is_activated: activate
1170
1797
  },
1171
1798
  timeout: 60000, // 60s
1172
1799
  validateStatus: function (status) {
@@ -1176,12 +1803,16 @@ class MCPServer {
1176
1803
 
1177
1804
  const response = await axios(config);
1178
1805
 
1179
- this.log('info', `Successfully triggered file extraction for ${domain}`);
1806
+ if (response.status !== 200) {
1807
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1808
+ }
1809
+
1810
+ this.log('info', `Successfully triggered theme deployment for ${domain}`);
1180
1811
  return true;
1181
1812
 
1182
1813
  } catch (error) {
1183
1814
  const errorMessage = error instanceof Error ? error.message : String(error);
1184
- this.log('error', `Failed to trigger file extraction: ${errorMessage}`);
1815
+ this.log('error', `Failed to trigger theme deployment: ${errorMessage}`);
1185
1816
 
1186
1817
  if (axios.isAxiosError(error)) {
1187
1818
  const responseData = error.response?.data;
@@ -1196,28 +1827,26 @@ class MCPServer {
1196
1827
  }
1197
1828
  }
1198
1829
 
1199
- async handleWordpressWebsiteImport(params) {
1200
- const { domain, archivePath, databaseDump } = params;
1830
+ async handleWordpressThemeDeploy(params) {
1831
+ const { domain, slug, themePath, activate = false } = params;
1201
1832
 
1202
- this.hosting_importWordpressWebsite_validateRequiredParams(params);
1203
- this.hosting_importWordpressWebsite_validateArchiveFile(archivePath);
1204
- this.hosting_importWordpressWebsite_validateDatabaseFile(databaseDump);
1833
+ this.hosting_deployWordpressTheme_validateRequiredParams(params);
1834
+ this.hosting_deployWordpressTheme_validateThemeDirectory(themePath);
1205
1835
 
1206
- // Auto-resolve username from domain
1207
1836
  this.log('info', `Resolving username from domain: ${domain}`);
1208
1837
  const username = await this.resolveUsername(domain);
1209
1838
 
1210
- await this.hosting_importWordpressWebsite_checkWebsiteIsEmpty(username, domain);
1839
+ const randomSuffix = this.hosting_deployWordpressTheme_generateRandomString(8);
1840
+ const uploadDirName = `${slug}-${randomSuffix}`;
1211
1841
 
1212
- const filesToUpload = [{
1213
- absolutePath: archivePath,
1214
- relativePath: path.basename(archivePath),
1215
- type: 'archive'
1216
- }, {
1217
- absolutePath: databaseDump,
1218
- relativePath: path.basename(databaseDump),
1219
- type: 'database'
1220
- }];
1842
+ this.log('info', `Scanning theme directory: ${themePath}`);
1843
+ const themeFiles = this.hosting_deployWordpressTheme_scanDirectory(themePath);
1844
+
1845
+ if (themeFiles.length === 0) {
1846
+ throw new Error(`No files found in theme directory: ${themePath}`);
1847
+ }
1848
+
1849
+ this.log('info', `Found ${themeFiles.length} files to upload`);
1221
1850
 
1222
1851
  let uploadCredentials;
1223
1852
  try {
@@ -1233,20 +1862,22 @@ class MCPServer {
1233
1862
  throw new Error('Invalid upload credentials received from API');
1234
1863
  }
1235
1864
 
1236
- this.log('info', `Starting website archive import to ${uploadUrl}`);
1865
+ this.log('info', `Starting theme file upload to ${uploadUrl}`);
1237
1866
 
1238
1867
  const results = [];
1239
1868
  let successCount = 0;
1240
1869
  let failureCount = 0;
1241
1870
 
1242
- for (const fileInfo of filesToUpload) {
1871
+ for (const fileInfo of themeFiles) {
1243
1872
  try {
1244
- this.log('info', `Uploading ${fileInfo.type}: ${fileInfo.absolutePath}`);
1873
+ const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
1874
+ const uploadPath = `wp-content/themes/${uploadDirName}/${normalizedRelativePath}`;
1875
+ this.log('info', `Uploading: ${fileInfo.absolutePath} -> ${uploadPath}`);
1245
1876
 
1246
1877
  const stats = fs.statSync(fileInfo.absolutePath);
1247
1878
  const uploadResult = await this.uploadFile(
1248
1879
  fileInfo.absolutePath,
1249
- fileInfo.relativePath,
1880
+ uploadPath,
1250
1881
  uploadUrl,
1251
1882
  authRestToken,
1252
1883
  authToken
@@ -1254,28 +1885,29 @@ class MCPServer {
1254
1885
 
1255
1886
  results.push({
1256
1887
  file: fileInfo.absolutePath,
1257
- remotePath: fileInfo.relativePath,
1258
- type: fileInfo.type,
1888
+ remotePath: uploadPath,
1259
1889
  status: 'success',
1260
1890
  uploadUrl: uploadResult.url,
1261
1891
  size: stats.size
1262
1892
  });
1263
1893
 
1264
1894
  successCount++;
1265
- this.log('info', `Successfully uploaded ${fileInfo.type}: ${fileInfo.relativePath}`);
1895
+ this.log('info', `Successfully uploaded: ${uploadPath}`);
1266
1896
 
1267
1897
  } catch (error) {
1268
1898
  const errorMessage = error instanceof Error ? error.message : String(error);
1899
+ const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
1900
+ const uploadPath = `wp-content/themes/${uploadDirName}/${normalizedRelativePath}`;
1901
+
1269
1902
  results.push({
1270
1903
  file: fileInfo.absolutePath,
1271
- remotePath: fileInfo.relativePath,
1272
- type: fileInfo.type,
1904
+ remotePath: uploadPath,
1273
1905
  status: 'error',
1274
1906
  error: errorMessage
1275
1907
  });
1276
1908
 
1277
1909
  failureCount++;
1278
- this.log('error', `Failed to upload ${fileInfo.type} ${fileInfo.absolutePath}: ${errorMessage}`);
1910
+ this.log('error', `Failed to upload ${fileInfo.absolutePath}: ${errorMessage}`);
1279
1911
  }
1280
1912
  }
1281
1913
 
@@ -1283,20 +1915,20 @@ class MCPServer {
1283
1915
 
1284
1916
  if (failureCount === 0) {
1285
1917
  try {
1286
- this.log('info', 'All files uploaded successfully, triggering extraction...');
1287
- await this.hosting_importWordpressWebsite_extractFiles(username, domain, archivePath, databaseDump);
1918
+ this.log('info', 'All files uploaded successfully, triggering theme deployment...');
1919
+ await this.hosting_deployWordpressTheme_deployTheme(username, domain, slug, uploadDirName, activate);
1288
1920
  } catch (error) {
1289
1921
  const errorMessage = error instanceof Error ? error.message : String(error);
1290
- this.log('error', `File extraction failed: ${errorMessage}`);
1922
+ this.log('error', `Theme deployment failed: ${errorMessage}`);
1291
1923
  return {
1292
1924
  status: 'partial',
1293
1925
  summary: {
1294
- total: filesToUpload.length,
1926
+ total: themeFiles.length,
1295
1927
  successful: successCount,
1296
1928
  failed: failureCount
1297
1929
  },
1298
1930
  results,
1299
- extractionError: errorMessage
1931
+ deploymentError: errorMessage
1300
1932
  };
1301
1933
  }
1302
1934
  }
@@ -1304,83 +1936,142 @@ class MCPServer {
1304
1936
  return {
1305
1937
  status: overallStatus,
1306
1938
  summary: {
1307
- total: filesToUpload.length,
1939
+ total: themeFiles.length,
1308
1940
  successful: successCount,
1309
1941
  failed: failureCount
1310
1942
  },
1311
- results
1943
+ results,
1944
+ uploadDirName,
1945
+ activated: activate
1312
1946
  };
1313
1947
  }
1314
1948
 
1315
- hosting_deployWordpressPlugin_generateRandomString(length) {
1316
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1317
- let result = '';
1318
- for (let i = 0; i < length; i++) {
1319
- result += chars.charAt(Math.floor(Math.random() * chars.length));
1949
+ hosting_deployJsApplication_validateArchiveFormat(filePath) {
1950
+ const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
1951
+ const fileName = path.basename(filePath).toLowerCase();
1952
+
1953
+ for (const ext of validExtensions) {
1954
+ if (fileName.endsWith(`.${ext}`)) {
1955
+ return true;
1956
+ }
1320
1957
  }
1321
- return result;
1958
+
1959
+ return false;
1322
1960
  }
1323
1961
 
1324
- hosting_deployWordpressPlugin_validateRequiredParams(params) {
1325
- const { domain, slug, pluginPath } = params;
1962
+ hosting_deployJsApplication_validateRequiredParams(params) {
1963
+ const { domain, archivePath, removeArchive } = params;
1326
1964
 
1327
1965
  if (!domain || typeof domain !== 'string') {
1328
1966
  throw new Error('domain is required and must be a string');
1329
1967
  }
1330
1968
 
1331
- if (!slug || typeof slug !== 'string') {
1332
- throw new Error('slug is required and must be a string');
1969
+ if (!archivePath || typeof archivePath !== 'string') {
1970
+ throw new Error('archivePath is required and must be a string');
1333
1971
  }
1334
1972
 
1335
- if (!pluginPath || typeof pluginPath !== 'string') {
1336
- throw new Error('pluginPath is required and must be a string');
1973
+ if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
1974
+ throw new Error('removeArchive must be a boolean if provided');
1337
1975
  }
1338
1976
  }
1339
1977
 
1340
- hosting_deployWordpressPlugin_validatePluginDirectory(pluginPath) {
1341
- if (!fs.existsSync(pluginPath)) {
1342
- throw new Error(`Plugin directory not found: ${pluginPath}`);
1978
+ hosting_deployJsApplication_removeArchive(archivePath, removeArchive) {
1979
+ if (!removeArchive) {
1980
+ return false;
1343
1981
  }
1344
1982
 
1345
- const pluginStats = fs.statSync(pluginPath);
1346
- if (!pluginStats.isDirectory()) {
1347
- throw new Error(`Plugin path is not a directory: ${pluginPath}`);
1983
+ try {
1984
+ this.log('info', `Removing archive file: ${archivePath}`);
1985
+ fs.unlinkSync(archivePath);
1986
+ this.log('info', `Successfully removed archive file: ${archivePath}`);
1987
+ return true;
1988
+ } catch (error) {
1989
+ const errorMessage = error instanceof Error ? error.message : String(error);
1990
+ this.log('error', `Failed to remove archive file: ${errorMessage}`);
1991
+ // Don't fail the entire operation if archive removal fails
1992
+ return false;
1348
1993
  }
1349
1994
  }
1350
1995
 
1351
- hosting_deployWordpressPlugin_scanDirectory(dirPath, basePath = dirPath) {
1352
- const files = [];
1353
-
1354
- const scanDir = (currentPath) => {
1355
- const items = fs.readdirSync(currentPath);
1356
-
1357
- for (const item of items) {
1358
- const itemPath = path.join(currentPath, item);
1359
- const stats = fs.statSync(itemPath);
1360
-
1361
- if (stats.isDirectory()) {
1362
- scanDir(itemPath);
1363
- } else if (stats.isFile()) {
1364
- const relativePath = path.relative(basePath, itemPath);
1365
- files.push({
1366
- absolutePath: itemPath,
1367
- relativePath: relativePath
1368
- });
1996
+ hosting_deployJsApplication_validateArchiveFile(archivePath) {
1997
+ if (!fs.existsSync(archivePath)) {
1998
+ throw new Error(`Archive file not found: ${archivePath}`);
1999
+ }
2000
+
2001
+ const archiveStats = fs.statSync(archivePath);
2002
+ if (!archiveStats.isFile()) {
2003
+ throw new Error(`Archive path is not a file: ${archivePath}`);
2004
+ }
2005
+
2006
+ if (!this.hosting_deployJsApplication_validateArchiveFormat(archivePath)) {
2007
+ throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
2008
+ }
2009
+ }
2010
+
2011
+ async hosting_deployJsApplication_fetchBuildSettings(username, domain, archivePath) {
2012
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
2013
+ const archiveBasename = path.basename(archivePath);
2014
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/settings/from-archive?archive_path=${encodeURIComponent(archiveBasename)}`, baseUrl).toString();
2015
+
2016
+ try {
2017
+ const bearerToken = await this.getAuthToken();
2018
+
2019
+ const config = {
2020
+ method: 'get',
2021
+ url,
2022
+ headers: {
2023
+ ...this.headers,
2024
+ 'Authorization': `Bearer ${bearerToken}`
2025
+ },
2026
+ timeout: 60000, // 60s
2027
+ validateStatus: function (status) {
2028
+ return status < 500;
1369
2029
  }
2030
+ };
2031
+
2032
+ const response = await axios(config);
2033
+
2034
+ if (response.status !== 200) {
2035
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
2036
+ }
2037
+
2038
+ this.log('info', `Successfully fetched build settings for ${domain}`);
2039
+ return response.data;
2040
+
2041
+ } catch (error) {
2042
+ const errorMessage = error instanceof Error ? error.message : String(error);
2043
+ this.log('error', `Failed to fetch build settings: ${errorMessage}`);
2044
+
2045
+ if (axios.isAxiosError(error)) {
2046
+ const responseData = error.response?.data;
2047
+ const responseStatus = error.response?.status;
2048
+ this.log('error', 'API Error Details:', {
2049
+ status: responseStatus,
2050
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
2051
+ });
1370
2052
  }
1371
- };
1372
-
1373
- scanDir(dirPath);
1374
- return files;
2053
+
2054
+ throw error;
2055
+ }
1375
2056
  }
1376
2057
 
1377
- async hosting_deployWordpressPlugin_deployPlugin(username, domain, slug, pluginPath) {
2058
+ async hosting_deployJsApplication_triggerBuild(username, domain, archivePath, buildSettings) {
1378
2059
  const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1379
- const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/plugins/deploy`, baseUrl).toString();
2060
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
1380
2061
 
1381
2062
  try {
1382
2063
  const bearerToken = await this.getAuthToken();
1383
2064
 
2065
+ const archiveBasename = path.basename(archivePath);
2066
+ const buildData = {
2067
+ ...buildSettings,
2068
+ node_version: buildSettings?.node_version || 20,
2069
+ source_type: 'archive',
2070
+ source_options: {
2071
+ archive_path: archiveBasename
2072
+ }
2073
+ };
2074
+
1384
2075
  const config = {
1385
2076
  method: 'post',
1386
2077
  url,
@@ -1389,10 +2080,7 @@ class MCPServer {
1389
2080
  'Authorization': `Bearer ${bearerToken}`,
1390
2081
  'Content-Type': 'application/json'
1391
2082
  },
1392
- data: {
1393
- slug,
1394
- plugin_path: pluginPath
1395
- },
2083
+ data: buildData,
1396
2084
  timeout: 60000, // 60s
1397
2085
  validateStatus: function (status) {
1398
2086
  return status < 500;
@@ -1405,12 +2093,12 @@ class MCPServer {
1405
2093
  throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1406
2094
  }
1407
2095
 
1408
- this.log('info', `Successfully triggered plugin deployment for ${domain}`);
1409
- return true;
2096
+ this.log('info', `Successfully triggered build for ${domain}`);
2097
+ return response.data;
1410
2098
 
1411
2099
  } catch (error) {
1412
2100
  const errorMessage = error instanceof Error ? error.message : String(error);
1413
- this.log('error', `Failed to trigger plugin deployment: ${errorMessage}`);
2101
+ this.log('error', `Failed to trigger build: ${errorMessage}`);
1414
2102
 
1415
2103
  if (axios.isAxiosError(error)) {
1416
2104
  const responseData = error.response?.data;
@@ -1425,27 +2113,19 @@ class MCPServer {
1425
2113
  }
1426
2114
  }
1427
2115
 
1428
- async handleWordpressPluginDeploy(params) {
1429
- const { domain, slug, pluginPath } = params;
2116
+ async handleJavascriptApplicationDeploy(params) {
2117
+ const { domain, archivePath, removeArchive = false } = params;
1430
2118
 
1431
- this.hosting_deployWordpressPlugin_validateRequiredParams(params);
1432
- this.hosting_deployWordpressPlugin_validatePluginDirectory(pluginPath);
2119
+ this.hosting_deployJsApplication_validateRequiredParams(params);
2120
+ this.hosting_deployJsApplication_validateArchiveFile(archivePath);
1433
2121
 
2122
+ // Auto-resolve username from domain
1434
2123
  this.log('info', `Resolving username from domain: ${domain}`);
1435
2124
  const username = await this.resolveUsername(domain);
1436
2125
 
1437
- const randomSuffix = this.hosting_deployWordpressPlugin_generateRandomString(8);
1438
- const uploadDirName = `${slug}-${randomSuffix}`;
1439
-
1440
- this.log('info', `Scanning plugin directory: ${pluginPath}`);
1441
- const pluginFiles = this.hosting_deployWordpressPlugin_scanDirectory(pluginPath);
1442
-
1443
- if (pluginFiles.length === 0) {
1444
- throw new Error(`No files found in plugin directory: ${pluginPath}`);
1445
- }
1446
-
1447
- this.log('info', `Found ${pluginFiles.length} files to upload`);
1448
-
2126
+ // Upload archive file
2127
+ this.log('info', `Starting archive upload for ${domain}`);
2128
+
1449
2129
  let uploadCredentials;
1450
2130
  try {
1451
2131
  uploadCredentials = await this.fetchUploadCredentials(username, domain);
@@ -1460,158 +2140,181 @@ class MCPServer {
1460
2140
  throw new Error('Invalid upload credentials received from API');
1461
2141
  }
1462
2142
 
1463
- this.log('info', `Starting plugin file upload to ${uploadUrl}`);
1464
-
1465
- const results = [];
1466
- let successCount = 0;
1467
- let failureCount = 0;
2143
+ const archiveBasename = path.basename(archivePath);
2144
+ let uploadResult;
2145
+ try {
2146
+ const stats = fs.statSync(archivePath);
2147
+ uploadResult = await this.uploadFile(
2148
+ archivePath,
2149
+ archiveBasename,
2150
+ uploadUrl,
2151
+ authRestToken,
2152
+ authToken
2153
+ );
1468
2154
 
1469
- for (const fileInfo of pluginFiles) {
1470
- try {
1471
- const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
1472
- const uploadPath = `wp-content/plugins/${uploadDirName}/${normalizedRelativePath}`;
1473
- this.log('info', `Uploading: ${fileInfo.absolutePath} -> ${uploadPath}`);
2155
+ this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
2156
+ } catch (error) {
2157
+ const errorMessage = error instanceof Error ? error.message : String(error);
2158
+ throw new Error(`Failed to upload archive: ${errorMessage}`);
2159
+ }
1474
2160
 
1475
- const stats = fs.statSync(fileInfo.absolutePath);
1476
- const uploadResult = await this.uploadFile(
1477
- fileInfo.absolutePath,
1478
- uploadPath,
1479
- uploadUrl,
1480
- authRestToken,
1481
- authToken
1482
- );
2161
+ // Fetch build settings
2162
+ let buildSettings;
2163
+ try {
2164
+ this.log('info', `Fetching build settings for ${domain}`);
2165
+ buildSettings = await this.hosting_deployJsApplication_fetchBuildSettings(username, domain, archivePath);
2166
+ } catch (error) {
2167
+ const errorMessage = error instanceof Error ? error.message : String(error);
2168
+ this.log('error', `Failed to fetch build settings: ${errorMessage}`);
2169
+ const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
1483
2170
 
1484
- results.push({
1485
- file: fileInfo.absolutePath,
1486
- remotePath: uploadPath,
2171
+ return {
2172
+ upload: {
1487
2173
  status: 'success',
1488
- uploadUrl: uploadResult.url,
1489
- size: stats.size
1490
- });
1491
-
1492
- successCount++;
1493
- this.log('info', `Successfully uploaded: ${uploadPath}`);
1494
-
1495
- } catch (error) {
1496
- const errorMessage = error instanceof Error ? error.message : String(error);
1497
- const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
1498
- const uploadPath = `wp-content/plugins/${uploadDirName}/${normalizedRelativePath}`;
1499
-
1500
- results.push({
1501
- file: fileInfo.absolutePath,
1502
- remotePath: uploadPath,
2174
+ data: {
2175
+ filename: uploadResult.filename
2176
+ }
2177
+ },
2178
+ resolveSettings: {
1503
2179
  status: 'error',
1504
2180
  error: errorMessage
1505
- });
1506
-
1507
- failureCount++;
1508
- this.log('error', `Failed to upload ${fileInfo.absolutePath}: ${errorMessage}`);
1509
- }
2181
+ },
2182
+ build: {
2183
+ status: 'skipped'
2184
+ },
2185
+ removeArchive: {
2186
+ status: archiveRemoved ? 'success' : 'skipped'
2187
+ }
2188
+ };
1510
2189
  }
1511
2190
 
1512
- const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
2191
+ // Trigger build
2192
+ let buildResult;
2193
+ try {
2194
+ this.log('info', `Triggering build for ${domain}`);
2195
+ buildResult = await this.hosting_deployJsApplication_triggerBuild(username, domain, archivePath, buildSettings);
2196
+ } catch (error) {
2197
+ const errorMessage = error instanceof Error ? error.message : String(error);
2198
+ this.log('error', `Failed to trigger build: ${errorMessage}`);
2199
+ const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
1513
2200
 
1514
- if (failureCount === 0) {
1515
- try {
1516
- this.log('info', 'All files uploaded successfully, triggering plugin deployment...');
1517
- await this.hosting_deployWordpressPlugin_deployPlugin(username, domain, slug, uploadDirName);
1518
- } catch (error) {
1519
- const errorMessage = error instanceof Error ? error.message : String(error);
1520
- this.log('error', `Plugin deployment failed: ${errorMessage}`);
1521
- return {
1522
- status: 'partial',
1523
- summary: {
1524
- total: pluginFiles.length,
1525
- successful: successCount,
1526
- failed: failureCount
1527
- },
1528
- results,
1529
- deploymentError: errorMessage
1530
- };
1531
- }
2201
+ return {
2202
+ upload: {
2203
+ status: 'success',
2204
+ data: {
2205
+ filename: uploadResult.filename
2206
+ }
2207
+ },
2208
+ resolveSettings: {
2209
+ status: 'success',
2210
+ data: buildSettings
2211
+ },
2212
+ build: {
2213
+ status: 'error',
2214
+ error: errorMessage
2215
+ },
2216
+ removeArchive: {
2217
+ status: archiveRemoved ? 'success' : 'skipped'
2218
+ }
2219
+ };
1532
2220
  }
1533
2221
 
2222
+ const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
2223
+
1534
2224
  return {
1535
- status: overallStatus,
1536
- summary: {
1537
- total: pluginFiles.length,
1538
- successful: successCount,
1539
- failed: failureCount
2225
+ upload: {
2226
+ status: 'success',
2227
+ data: {
2228
+ filename: uploadResult.filename
2229
+ }
1540
2230
  },
1541
- results,
1542
- uploadDirName
2231
+ resolveSettings: {
2232
+ status: 'success',
2233
+ data: buildSettings
2234
+ },
2235
+ build: {
2236
+ status: 'success',
2237
+ data: buildResult
2238
+ },
2239
+ removeArchive: {
2240
+ status: archiveRemoved ? 'success' : 'skipped'
2241
+ }
1543
2242
  };
1544
2243
  }
1545
2244
 
1546
- hosting_deployWordpressTheme_generateRandomString(length) {
1547
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1548
- let result = '';
1549
- for (let i = 0; i < length; i++) {
1550
- result += chars.charAt(Math.floor(Math.random() * chars.length));
2245
+ hosting_deployStaticWebsite_validateArchiveFormat(filePath) {
2246
+ const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
2247
+ const fileName = path.basename(filePath).toLowerCase();
2248
+
2249
+ for (const ext of validExtensions) {
2250
+ if (fileName.endsWith(`.${ext}`)) {
2251
+ return true;
2252
+ }
1551
2253
  }
1552
- return result;
2254
+
2255
+ return false;
1553
2256
  }
1554
2257
 
1555
- hosting_deployWordpressTheme_validateRequiredParams(params) {
1556
- const { domain, slug, themePath } = params;
2258
+ hosting_deployStaticWebsite_validateRequiredParams(params) {
2259
+ const { domain, archivePath, removeArchive } = params;
1557
2260
 
1558
2261
  if (!domain || typeof domain !== 'string') {
1559
2262
  throw new Error('domain is required and must be a string');
1560
2263
  }
1561
2264
 
1562
- if (!slug || typeof slug !== 'string') {
1563
- throw new Error('slug is required and must be a string');
2265
+ if (!archivePath || typeof archivePath !== 'string') {
2266
+ throw new Error('archivePath is required and must be a string');
1564
2267
  }
1565
2268
 
1566
- if (!themePath || typeof themePath !== 'string') {
1567
- throw new Error('themePath is required and must be a string');
2269
+ if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
2270
+ throw new Error('removeArchive must be a boolean if provided');
2271
+ }
2272
+ }
2273
+
2274
+ hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive) {
2275
+ if (!removeArchive) {
2276
+ return false;
2277
+ }
2278
+
2279
+ try {
2280
+ this.log('info', `Removing archive file: ${archivePath}`);
2281
+ fs.unlinkSync(archivePath);
2282
+ this.log('info', `Successfully removed archive file: ${archivePath}`);
2283
+ return true;
2284
+ } catch (error) {
2285
+ const errorMessage = error instanceof Error ? error.message : String(error);
2286
+ this.log('error', `Failed to remove archive file: ${errorMessage}`);
2287
+ return false;
1568
2288
  }
1569
2289
  }
1570
2290
 
1571
- hosting_deployWordpressTheme_validateThemeDirectory(themePath) {
1572
- if (!fs.existsSync(themePath)) {
1573
- throw new Error(`Theme directory not found: ${themePath}`);
2291
+ hosting_deployStaticWebsite_validateArchiveFile(archivePath) {
2292
+ if (!fs.existsSync(archivePath)) {
2293
+ throw new Error(`Archive file not found: ${archivePath}`);
1574
2294
  }
1575
2295
 
1576
- const themeStats = fs.statSync(themePath);
1577
- if (!themeStats.isDirectory()) {
1578
- throw new Error(`Theme path is not a directory: ${themePath}`);
2296
+ const archiveStats = fs.statSync(archivePath);
2297
+ if (!archiveStats.isFile()) {
2298
+ throw new Error(`Archive path is not a file: ${archivePath}`);
1579
2299
  }
1580
- }
1581
2300
 
1582
- hosting_deployWordpressTheme_scanDirectory(dirPath, basePath = dirPath) {
1583
- const files = [];
1584
-
1585
- const scanDir = (currentPath) => {
1586
- const items = fs.readdirSync(currentPath);
1587
-
1588
- for (const item of items) {
1589
- const itemPath = path.join(currentPath, item);
1590
- const stats = fs.statSync(itemPath);
1591
-
1592
- if (stats.isDirectory()) {
1593
- scanDir(itemPath);
1594
- } else if (stats.isFile()) {
1595
- const relativePath = path.relative(basePath, itemPath);
1596
- files.push({
1597
- absolutePath: itemPath,
1598
- relativePath: relativePath
1599
- });
1600
- }
1601
- }
1602
- };
1603
-
1604
- scanDir(dirPath);
1605
- return files;
2301
+ if (!this.hosting_deployStaticWebsite_validateArchiveFormat(archivePath)) {
2302
+ throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
2303
+ }
1606
2304
  }
1607
2305
 
1608
- async hosting_deployWordpressTheme_deployTheme(username, domain, slug, themePath, activate = false) {
2306
+ async hosting_deployStaticWebsite_triggerDeploy(username, domain, archivePath) {
1609
2307
  const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1610
- const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/themes/deploy`, baseUrl).toString();
2308
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/deploy`, baseUrl).toString();
1611
2309
 
1612
2310
  try {
1613
2311
  const bearerToken = await this.getAuthToken();
1614
2312
 
2313
+ const archiveBasename = path.basename(archivePath);
2314
+ const deployData = {
2315
+ archive_path: archiveBasename
2316
+ };
2317
+
1615
2318
  const config = {
1616
2319
  method: 'post',
1617
2320
  url,
@@ -1620,12 +2323,8 @@ class MCPServer {
1620
2323
  'Authorization': `Bearer ${bearerToken}`,
1621
2324
  'Content-Type': 'application/json'
1622
2325
  },
1623
- data: {
1624
- slug,
1625
- theme_path: themePath,
1626
- is_activated: activate
1627
- },
1628
- timeout: 60000, // 60s
2326
+ data: deployData,
2327
+ timeout: 60000,
1629
2328
  validateStatus: function (status) {
1630
2329
  return status < 500;
1631
2330
  }
@@ -1637,12 +2336,12 @@ class MCPServer {
1637
2336
  throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1638
2337
  }
1639
2338
 
1640
- this.log('info', `Successfully triggered theme deployment for ${domain}`);
1641
- return true;
2339
+ this.log('info', `Successfully triggered deployment for ${domain}`);
2340
+ return response.data;
1642
2341
 
1643
2342
  } catch (error) {
1644
2343
  const errorMessage = error instanceof Error ? error.message : String(error);
1645
- this.log('error', `Failed to trigger theme deployment: ${errorMessage}`);
2344
+ this.log('error', `Failed to trigger deployment: ${errorMessage}`);
1646
2345
 
1647
2346
  if (axios.isAxiosError(error)) {
1648
2347
  const responseData = error.response?.data;
@@ -1657,27 +2356,17 @@ class MCPServer {
1657
2356
  }
1658
2357
  }
1659
2358
 
1660
- async handleWordpressThemeDeploy(params) {
1661
- const { domain, slug, themePath, activate = false } = params;
2359
+ async handleStaticWebsiteDeploy(params) {
2360
+ const { domain, archivePath, removeArchive = false } = params;
1662
2361
 
1663
- this.hosting_deployWordpressTheme_validateRequiredParams(params);
1664
- this.hosting_deployWordpressTheme_validateThemeDirectory(themePath);
2362
+ this.hosting_deployStaticWebsite_validateRequiredParams(params);
2363
+ this.hosting_deployStaticWebsite_validateArchiveFile(archivePath);
1665
2364
 
1666
2365
  this.log('info', `Resolving username from domain: ${domain}`);
1667
2366
  const username = await this.resolveUsername(domain);
1668
2367
 
1669
- const randomSuffix = this.hosting_deployWordpressTheme_generateRandomString(8);
1670
- const uploadDirName = `${slug}-${randomSuffix}`;
1671
-
1672
- this.log('info', `Scanning theme directory: ${themePath}`);
1673
- const themeFiles = this.hosting_deployWordpressTheme_scanDirectory(themePath);
1674
-
1675
- if (themeFiles.length === 0) {
1676
- throw new Error(`No files found in theme directory: ${themePath}`);
1677
- }
1678
-
1679
- this.log('info', `Found ${themeFiles.length} files to upload`);
1680
-
2368
+ this.log('info', `Starting archive upload for ${domain}`);
2369
+
1681
2370
  let uploadCredentials;
1682
2371
  try {
1683
2372
  uploadCredentials = await this.fetchUploadCredentials(username, domain);
@@ -1692,163 +2381,110 @@ class MCPServer {
1692
2381
  throw new Error('Invalid upload credentials received from API');
1693
2382
  }
1694
2383
 
1695
- this.log('info', `Starting theme file upload to ${uploadUrl}`);
1696
-
1697
- const results = [];
1698
- let successCount = 0;
1699
- let failureCount = 0;
2384
+ const archiveBasename = path.basename(archivePath);
2385
+ let uploadResult;
2386
+ try {
2387
+ const stats = fs.statSync(archivePath);
2388
+ uploadResult = await this.uploadFile(
2389
+ archivePath,
2390
+ archiveBasename,
2391
+ uploadUrl,
2392
+ authRestToken,
2393
+ authToken
2394
+ );
1700
2395
 
1701
- for (const fileInfo of themeFiles) {
1702
- try {
1703
- const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
1704
- const uploadPath = `wp-content/themes/${uploadDirName}/${normalizedRelativePath}`;
1705
- this.log('info', `Uploading: ${fileInfo.absolutePath} -> ${uploadPath}`);
2396
+ this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
2397
+ } catch (error) {
2398
+ const errorMessage = error instanceof Error ? error.message : String(error);
2399
+ throw new Error(`Failed to upload archive: ${errorMessage}`);
2400
+ }
1706
2401
 
1707
- const stats = fs.statSync(fileInfo.absolutePath);
1708
- const uploadResult = await this.uploadFile(
1709
- fileInfo.absolutePath,
1710
- uploadPath,
1711
- uploadUrl,
1712
- authRestToken,
1713
- authToken
1714
- );
2402
+ let deployResult;
2403
+ try {
2404
+ this.log('info', `Triggering deployment for ${domain}`);
2405
+ deployResult = await this.hosting_deployStaticWebsite_triggerDeploy(username, domain, archivePath);
2406
+ } catch (error) {
2407
+ const errorMessage = error instanceof Error ? error.message : String(error);
2408
+ this.log('error', `Failed to trigger deployment: ${errorMessage}`);
2409
+ const archiveRemoved = this.hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive);
1715
2410
 
1716
- results.push({
1717
- file: fileInfo.absolutePath,
1718
- remotePath: uploadPath,
2411
+ return {
2412
+ upload: {
1719
2413
  status: 'success',
1720
- uploadUrl: uploadResult.url,
1721
- size: stats.size
1722
- });
1723
-
1724
- successCount++;
1725
- this.log('info', `Successfully uploaded: ${uploadPath}`);
1726
-
1727
- } catch (error) {
1728
- const errorMessage = error instanceof Error ? error.message : String(error);
1729
- const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
1730
- const uploadPath = `wp-content/themes/${uploadDirName}/${normalizedRelativePath}`;
1731
-
1732
- results.push({
1733
- file: fileInfo.absolutePath,
1734
- remotePath: uploadPath,
2414
+ data: {
2415
+ filename: uploadResult.filename
2416
+ }
2417
+ },
2418
+ deploy: {
1735
2419
  status: 'error',
1736
2420
  error: errorMessage
1737
- });
1738
-
1739
- failureCount++;
1740
- this.log('error', `Failed to upload ${fileInfo.absolutePath}: ${errorMessage}`);
1741
- }
2421
+ },
2422
+ removeArchive: {
2423
+ status: archiveRemoved ? 'success' : 'skipped'
2424
+ }
2425
+ };
1742
2426
  }
1743
2427
 
1744
- const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
1745
-
1746
- if (failureCount === 0) {
1747
- try {
1748
- this.log('info', 'All files uploaded successfully, triggering theme deployment...');
1749
- await this.hosting_deployWordpressTheme_deployTheme(username, domain, slug, uploadDirName, activate);
1750
- } catch (error) {
1751
- const errorMessage = error instanceof Error ? error.message : String(error);
1752
- this.log('error', `Theme deployment failed: ${errorMessage}`);
1753
- return {
1754
- status: 'partial',
1755
- summary: {
1756
- total: themeFiles.length,
1757
- successful: successCount,
1758
- failed: failureCount
1759
- },
1760
- results,
1761
- deploymentError: errorMessage
1762
- };
1763
- }
1764
- }
2428
+ const archiveRemoved = this.hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive);
1765
2429
 
1766
2430
  return {
1767
- status: overallStatus,
1768
- summary: {
1769
- total: themeFiles.length,
1770
- successful: successCount,
1771
- failed: failureCount
2431
+ upload: {
2432
+ status: 'success',
2433
+ data: {
2434
+ filename: uploadResult.filename
2435
+ }
1772
2436
  },
1773
- results,
1774
- uploadDirName,
1775
- activated: activate
1776
- };
1777
- }
1778
-
1779
- hosting_deployJsApplication_validateArchiveFormat(filePath) {
1780
- const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
1781
- const fileName = path.basename(filePath).toLowerCase();
1782
-
1783
- for (const ext of validExtensions) {
1784
- if (fileName.endsWith(`.${ext}`)) {
1785
- return true;
2437
+ deploy: {
2438
+ status: 'success',
2439
+ data: deployResult
2440
+ },
2441
+ removeArchive: {
2442
+ status: archiveRemoved ? 'success' : 'skipped'
1786
2443
  }
1787
- }
1788
-
1789
- return false;
1790
- }
1791
-
1792
- hosting_deployJsApplication_validateRequiredParams(params) {
1793
- const { domain, archivePath, removeArchive } = params;
1794
-
1795
- if (!domain || typeof domain !== 'string') {
1796
- throw new Error('domain is required and must be a string');
1797
- }
1798
-
1799
- if (!archivePath || typeof archivePath !== 'string') {
1800
- throw new Error('archivePath is required and must be a string');
1801
- }
1802
-
1803
- if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
1804
- throw new Error('removeArchive must be a boolean if provided');
1805
- }
2444
+ };
1806
2445
  }
1807
2446
 
1808
- hosting_deployJsApplication_removeArchive(archivePath, removeArchive) {
1809
- if (!removeArchive) {
1810
- return false;
1811
- }
1812
-
1813
- try {
1814
- this.log('info', `Removing archive file: ${archivePath}`);
1815
- fs.unlinkSync(archivePath);
1816
- this.log('info', `Successfully removed archive file: ${archivePath}`);
1817
- return true;
1818
- } catch (error) {
1819
- const errorMessage = error instanceof Error ? error.message : String(error);
1820
- this.log('error', `Failed to remove archive file: ${errorMessage}`);
1821
- // Don't fail the entire operation if archive removal fails
1822
- return false;
2447
+ hosting_listJsDeployments_validateRequiredParams(params) {
2448
+ const { domain } = params;
2449
+
2450
+ if (!domain || typeof domain !== 'string') {
2451
+ throw new Error('domain is required and must be a string');
1823
2452
  }
1824
2453
  }
1825
2454
 
1826
- hosting_deployJsApplication_validateArchiveFile(archivePath) {
1827
- if (!fs.existsSync(archivePath)) {
1828
- throw new Error(`Archive file not found: ${archivePath}`);
2455
+ hosting_listJsDeployments_buildQueryParams(params) {
2456
+ const { page, perPage, states } = params;
2457
+ const queryParams = new URLSearchParams();
2458
+
2459
+ if (page !== undefined && page !== null) {
2460
+ queryParams.append('page', page.toString());
1829
2461
  }
1830
2462
 
1831
- const archiveStats = fs.statSync(archivePath);
1832
- if (!archiveStats.isFile()) {
1833
- throw new Error(`Archive path is not a file: ${archivePath}`);
2463
+ if (perPage !== undefined && perPage !== null) {
2464
+ queryParams.append('per_page', perPage.toString());
1834
2465
  }
1835
2466
 
1836
- if (!this.hosting_deployJsApplication_validateArchiveFormat(archivePath)) {
1837
- throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
2467
+ if (states && Array.isArray(states) && states.length > 0) {
2468
+ states.forEach(state => {
2469
+ queryParams.append('states[]', state);
2470
+ });
1838
2471
  }
2472
+
2473
+ return queryParams.toString();
1839
2474
  }
1840
2475
 
1841
- async hosting_deployJsApplication_fetchBuildSettings(username, domain, archivePath) {
2476
+ async hosting_listJsDeployments_fetchDeployments(username, domain, queryParams) {
1842
2477
  const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1843
- const archiveBasename = path.basename(archivePath);
1844
- const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/settings/from-archive?archive_path=${encodeURIComponent(archiveBasename)}`, baseUrl).toString();
2478
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
2479
+
2480
+ const fullUrl = queryParams ? `${url}?${queryParams}` : url;
1845
2481
 
1846
2482
  try {
1847
2483
  const bearerToken = await this.getAuthToken();
1848
2484
 
1849
2485
  const config = {
1850
2486
  method: 'get',
1851
- url,
2487
+ url: fullUrl,
1852
2488
  headers: {
1853
2489
  ...this.headers,
1854
2490
  'Authorization': `Bearer ${bearerToken}`
@@ -1865,12 +2501,12 @@ class MCPServer {
1865
2501
  throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1866
2502
  }
1867
2503
 
1868
- this.log('info', `Successfully fetched build settings for ${domain}`);
2504
+ this.log('info', `Successfully fetched deployments for ${domain}`);
1869
2505
  return response.data;
1870
2506
 
1871
2507
  } catch (error) {
1872
2508
  const errorMessage = error instanceof Error ? error.message : String(error);
1873
- this.log('error', `Failed to fetch build settings: ${errorMessage}`);
2509
+ this.log('error', `Failed to fetch deployments: ${errorMessage}`);
1874
2510
 
1875
2511
  if (axios.isAxiosError(error)) {
1876
2512
  const responseData = error.response?.data;
@@ -1885,33 +2521,85 @@ class MCPServer {
1885
2521
  }
1886
2522
  }
1887
2523
 
1888
- async hosting_deployJsApplication_triggerBuild(username, domain, archivePath, buildSettings) {
2524
+ async handleListJavascriptDeployments(params) {
2525
+ const { domain, page, perPage, states } = params;
2526
+
2527
+ this.hosting_listJsDeployments_validateRequiredParams(params);
2528
+
2529
+ // Auto-resolve username from domain
2530
+ this.log('info', `Resolving username from domain: ${domain}`);
2531
+ const username = await this.resolveUsername(domain);
2532
+
2533
+ // Build query parameters
2534
+ const queryParams = this.hosting_listJsDeployments_buildQueryParams(params);
2535
+
2536
+ // Fetch deployments
2537
+ let deployments;
2538
+ try {
2539
+ this.log('info', `Fetching deployments for ${domain}`);
2540
+ deployments = await this.hosting_listJsDeployments_fetchDeployments(username, domain, queryParams);
2541
+ } catch (error) {
2542
+ const errorMessage = error instanceof Error ? error.message : String(error);
2543
+ this.log('error', `Failed to fetch deployments: ${errorMessage}`);
2544
+ throw error;
2545
+ }
2546
+
2547
+ return {
2548
+ status: 'success',
2549
+ domain,
2550
+ username,
2551
+ queryParams: {
2552
+ page,
2553
+ perPage,
2554
+ states
2555
+ },
2556
+ deployments
2557
+ };
2558
+ }
2559
+
2560
+ hosting_showJsDeploymentLogs_validateRequiredParams(params) {
2561
+ const { domain, buildUuid, fromLine } = params;
2562
+
2563
+ if (!domain || typeof domain !== 'string') {
2564
+ throw new Error('domain is required and must be a string');
2565
+ }
2566
+
2567
+ if (!buildUuid || typeof buildUuid !== 'string') {
2568
+ throw new Error('buildUuid is required and must be a string');
2569
+ }
2570
+
2571
+ if (fromLine !== undefined && (typeof fromLine !== 'number' || !Number.isInteger(fromLine) || fromLine < 0)) {
2572
+ throw new Error('fromLine must be a non-negative integer when provided');
2573
+ }
2574
+ }
2575
+
2576
+ hosting_showJsDeploymentLogs_buildQueryParams(params) {
2577
+ const { fromLine } = params;
2578
+ const queryParams = new URLSearchParams();
2579
+
2580
+ const line = (typeof fromLine === 'number' && Number.isInteger(fromLine) && fromLine >= 0) ? fromLine : 0;
2581
+ queryParams.append('from_line', line.toString());
2582
+
2583
+ return queryParams.toString();
2584
+ }
2585
+
2586
+ async hosting_showJsDeploymentLogs_fetchLogs(username, domain, buildUuid, queryParams) {
1889
2587
  const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1890
- const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
2588
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/${buildUuid}/logs`, baseUrl).toString();
2589
+
2590
+ const fullUrl = queryParams ? `${url}?${queryParams}` : url;
1891
2591
 
1892
2592
  try {
1893
2593
  const bearerToken = await this.getAuthToken();
1894
2594
 
1895
- const archiveBasename = path.basename(archivePath);
1896
- const buildData = {
1897
- ...buildSettings,
1898
- node_version: buildSettings?.node_version || 20,
1899
- source_type: 'archive',
1900
- source_options: {
1901
- archive_path: archiveBasename
1902
- }
1903
- };
1904
-
1905
2595
  const config = {
1906
- method: 'post',
1907
- url,
2596
+ method: 'get',
2597
+ url: fullUrl,
1908
2598
  headers: {
1909
2599
  ...this.headers,
1910
- 'Authorization': `Bearer ${bearerToken}`,
1911
- 'Content-Type': 'application/json'
2600
+ 'Authorization': `Bearer ${bearerToken}`
1912
2601
  },
1913
- data: buildData,
1914
- timeout: 60000, // 60s
2602
+ timeout: 60000,
1915
2603
  validateStatus: function (status) {
1916
2604
  return status < 500;
1917
2605
  }
@@ -1923,12 +2611,12 @@ class MCPServer {
1923
2611
  throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1924
2612
  }
1925
2613
 
1926
- this.log('info', `Successfully triggered build for ${domain}`);
2614
+ this.log('info', `Successfully fetched logs for ${domain} build ${buildUuid}`);
1927
2615
  return response.data;
1928
2616
 
1929
2617
  } catch (error) {
1930
2618
  const errorMessage = error instanceof Error ? error.message : String(error);
1931
- this.log('error', `Failed to trigger build: ${errorMessage}`);
2619
+ this.log('error', `Failed to fetch logs: ${errorMessage}`);
1932
2620
 
1933
2621
  if (axios.isAxiosError(error)) {
1934
2622
  const responseData = error.response?.data;
@@ -1943,149 +2631,51 @@ class MCPServer {
1943
2631
  }
1944
2632
  }
1945
2633
 
1946
- async handleJavascriptApplicationDeploy(params) {
1947
- const { domain, archivePath, removeArchive = false } = params;
2634
+ async handleShowJsDeploymentLogs(params) {
2635
+ const { domain, buildUuid, fromLine } = params;
1948
2636
 
1949
- this.hosting_deployJsApplication_validateRequiredParams(params);
1950
- this.hosting_deployJsApplication_validateArchiveFile(archivePath);
2637
+ this.hosting_showJsDeploymentLogs_validateRequiredParams(params);
1951
2638
 
1952
- // Auto-resolve username from domain
1953
2639
  this.log('info', `Resolving username from domain: ${domain}`);
1954
2640
  const username = await this.resolveUsername(domain);
1955
2641
 
1956
- // Upload archive file
1957
- this.log('info', `Starting archive upload for ${domain}`);
1958
-
1959
- let uploadCredentials;
1960
- try {
1961
- uploadCredentials = await this.fetchUploadCredentials(username, domain);
1962
- } catch (error) {
1963
- const errorMessage = error instanceof Error ? error.message : String(error);
1964
- throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
1965
- }
1966
-
1967
- const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
1968
-
1969
- if (!uploadUrl || !authToken || !authRestToken) {
1970
- throw new Error('Invalid upload credentials received from API');
1971
- }
1972
-
1973
- const archiveBasename = path.basename(archivePath);
1974
- let uploadResult;
1975
- try {
1976
- const stats = fs.statSync(archivePath);
1977
- uploadResult = await this.uploadFile(
1978
- archivePath,
1979
- archiveBasename,
1980
- uploadUrl,
1981
- authRestToken,
1982
- authToken
1983
- );
1984
-
1985
- this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
1986
- } catch (error) {
1987
- const errorMessage = error instanceof Error ? error.message : String(error);
1988
- throw new Error(`Failed to upload archive: ${errorMessage}`);
1989
- }
1990
-
1991
- // Fetch build settings
1992
- let buildSettings;
1993
- try {
1994
- this.log('info', `Fetching build settings for ${domain}`);
1995
- buildSettings = await this.hosting_deployJsApplication_fetchBuildSettings(username, domain, archivePath);
1996
- } catch (error) {
1997
- const errorMessage = error instanceof Error ? error.message : String(error);
1998
- this.log('error', `Failed to fetch build settings: ${errorMessage}`);
1999
- const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
2000
-
2001
- return {
2002
- upload: {
2003
- status: 'success',
2004
- data: {
2005
- filename: uploadResult.filename
2006
- }
2007
- },
2008
- resolveSettings: {
2009
- status: 'error',
2010
- error: errorMessage
2011
- },
2012
- build: {
2013
- status: 'skipped'
2014
- },
2015
- removeArchive: {
2016
- status: archiveRemoved ? 'success' : 'skipped'
2017
- }
2018
- };
2019
- }
2642
+ const queryParams = this.hosting_showJsDeploymentLogs_buildQueryParams(params);
2020
2643
 
2021
- // Trigger build
2022
- let buildResult;
2644
+ let logs;
2023
2645
  try {
2024
- this.log('info', `Triggering build for ${domain}`);
2025
- buildResult = await this.hosting_deployJsApplication_triggerBuild(username, domain, archivePath, buildSettings);
2646
+ this.log('info', `Fetching logs for ${domain}, build ${buildUuid}`);
2647
+ logs = await this.hosting_showJsDeploymentLogs_fetchLogs(username, domain, buildUuid, queryParams);
2026
2648
  } catch (error) {
2027
2649
  const errorMessage = error instanceof Error ? error.message : String(error);
2028
- this.log('error', `Failed to trigger build: ${errorMessage}`);
2029
- const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
2030
-
2031
- return {
2032
- upload: {
2033
- status: 'success',
2034
- data: {
2035
- filename: uploadResult.filename
2036
- }
2037
- },
2038
- resolveSettings: {
2039
- status: 'success',
2040
- data: buildSettings
2041
- },
2042
- build: {
2043
- status: 'error',
2044
- error: errorMessage
2045
- },
2046
- removeArchive: {
2047
- status: archiveRemoved ? 'success' : 'skipped'
2048
- }
2049
- };
2650
+ this.log('error', `Failed to fetch logs: ${errorMessage}`);
2651
+ throw error;
2050
2652
  }
2051
2653
 
2052
- const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
2654
+ const effectiveFromLine = (typeof fromLine === 'number' && Number.isInteger(fromLine) && fromLine >= 0) ? fromLine : 0;
2053
2655
 
2054
2656
  return {
2055
- upload: {
2056
- status: 'success',
2057
- data: {
2058
- filename: uploadResult.filename
2059
- }
2060
- },
2061
- resolveSettings: {
2062
- status: 'success',
2063
- data: buildSettings
2064
- },
2065
- build: {
2066
- status: 'success',
2067
- data: buildResult
2068
- },
2069
- removeArchive: {
2070
- status: archiveRemoved ? 'success' : 'skipped'
2071
- }
2657
+ domain,
2658
+ username,
2659
+ buildUuid,
2660
+ fromLine: effectiveFromLine,
2661
+ logs
2072
2662
  };
2073
2663
  }
2074
2664
 
2075
- hosting_deployStaticWebsite_validateArchiveFormat(filePath) {
2076
- const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
2077
- const fileName = path.basename(filePath).toLowerCase();
2078
-
2079
- for (const ext of validExtensions) {
2080
- if (fileName.endsWith(`.${ext}`)) {
2081
- return true;
2082
- }
2665
+ agencyHosting_normalizePath(pathString) {
2666
+ return pathString.replace(/\\/g, '/');
2667
+ }
2668
+
2669
+ agencyHosting_generateUploadDir() {
2670
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
2671
+ let result = '';
2672
+ for (let i = 0; i < 12; i++) {
2673
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
2083
2674
  }
2084
-
2085
- return false;
2675
+ return result;
2086
2676
  }
2087
2677
 
2088
- hosting_deployStaticWebsite_validateRequiredParams(params) {
2678
+ agencyHosting_validateRequiredParams(params) {
2089
2679
  const { domain, archivePath, removeArchive } = params;
2090
2680
 
2091
2681
  if (!domain || typeof domain !== 'string') {
@@ -2101,7 +2691,18 @@ class MCPServer {
2101
2691
  }
2102
2692
  }
2103
2693
 
2104
- hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive) {
2694
+ agencyHosting_validateArchiveFile(archivePath) {
2695
+ if (!fs.existsSync(archivePath)) {
2696
+ throw new Error(`Archive file not found: ${archivePath}`);
2697
+ }
2698
+
2699
+ const archiveStats = fs.statSync(archivePath);
2700
+ if (!archiveStats.isFile()) {
2701
+ throw new Error(`Archive path is not a file: ${archivePath}`);
2702
+ }
2703
+ }
2704
+
2705
+ agencyHosting_removeArchive(archivePath, removeArchive) {
2105
2706
  if (!removeArchive) {
2106
2707
  return false;
2107
2708
  }
@@ -2118,33 +2719,208 @@ class MCPServer {
2118
2719
  }
2119
2720
  }
2120
2721
 
2121
- hosting_deployStaticWebsite_validateArchiveFile(archivePath) {
2122
- if (!fs.existsSync(archivePath)) {
2123
- throw new Error(`Archive file not found: ${archivePath}`);
2124
- }
2722
+ async agencyHosting_resolveWebsiteUid(domain) {
2723
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
2724
+ const perPage = 100;
2125
2725
 
2126
- const archiveStats = fs.statSync(archivePath);
2127
- if (!archiveStats.isFile()) {
2128
- throw new Error(`Archive path is not a file: ${archivePath}`);
2129
- }
2726
+ try {
2727
+ const bearerToken = await this.getAuthToken();
2728
+ let page = 1;
2729
+ let total = 0;
2130
2730
 
2131
- if (!this.hosting_deployStaticWebsite_validateArchiveFormat(archivePath)) {
2132
- throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
2731
+ do {
2732
+ const url = new URL(
2733
+ `api/agency-hosting/v1/domains?page=${page}&per_page=${perPage}`,
2734
+ baseUrl
2735
+ ).toString();
2736
+
2737
+ const config = {
2738
+ method: 'get',
2739
+ url,
2740
+ headers: {
2741
+ ...this.headers,
2742
+ 'Authorization': `Bearer ${bearerToken}`
2743
+ },
2744
+ timeout: 60000, // 60s
2745
+ validateStatus: function (status) {
2746
+ return status < 500;
2747
+ }
2748
+ };
2749
+
2750
+ const response = await axios(config);
2751
+
2752
+ if (response.status !== 200) {
2753
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
2754
+ }
2755
+
2756
+ const domains = response.data?.data ?? [];
2757
+ const match = domains.find((item) => item.fqdn === domain);
2758
+ if (match?.website_uid) {
2759
+ this.log('info', `Resolved website UID: ${match.website_uid} for domain: ${domain}`);
2760
+ return match.website_uid;
2761
+ }
2762
+
2763
+ total = response.data?.meta?.total ?? domains.length;
2764
+ page++;
2765
+ } while ((page - 1) * perPage < total);
2766
+
2767
+ throw new Error(`No Agency Plan website found for domain: ${domain}`);
2768
+
2769
+ } catch (error) {
2770
+ const errorMessage = error instanceof Error ? error.message : String(error);
2771
+ this.log('error', `Failed to resolve website UID for domain ${domain}: ${errorMessage}`);
2772
+
2773
+ if (axios.isAxiosError(error)) {
2774
+ const responseData = error.response?.data;
2775
+ const responseStatus = error.response?.status;
2776
+ this.log('error', 'API Error Details:', {
2777
+ status: responseStatus,
2778
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
2779
+ });
2780
+ }
2781
+
2782
+ throw error;
2133
2783
  }
2134
2784
  }
2135
2785
 
2136
- async hosting_deployStaticWebsite_triggerDeploy(username, domain, archivePath) {
2786
+ async agencyHosting_fetchUploadCredentials(websiteUid) {
2137
2787
  const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
2138
- const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/deploy`, baseUrl).toString();
2788
+ const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/files/upload-urls`, baseUrl).toString();
2139
2789
 
2140
2790
  try {
2141
2791
  const bearerToken = await this.getAuthToken();
2142
2792
 
2143
- const archiveBasename = path.basename(archivePath);
2144
- const deployData = {
2145
- archive_path: archiveBasename
2793
+ const config = {
2794
+ method: 'post',
2795
+ url,
2796
+ headers: {
2797
+ ...this.headers,
2798
+ 'Authorization': `Bearer ${bearerToken}`,
2799
+ 'Content-Type': 'application/json'
2800
+ },
2801
+ timeout: 60000, // 60s
2802
+ validateStatus: function (status) {
2803
+ return status < 500;
2804
+ }
2146
2805
  };
2147
2806
 
2807
+ const response = await axios(config);
2808
+
2809
+ if (response.status !== 200) {
2810
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
2811
+ }
2812
+
2813
+ return response.data;
2814
+
2815
+ } catch (error) {
2816
+ const errorMessage = error instanceof Error ? error.message : String(error);
2817
+ this.log('error', `Failed to fetch upload credentials: ${errorMessage}`);
2818
+
2819
+ if (axios.isAxiosError(error)) {
2820
+ const responseData = error.response?.data;
2821
+ const responseStatus = error.response?.status;
2822
+ this.log('error', 'API Error Details:', {
2823
+ status: responseStatus,
2824
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
2825
+ });
2826
+ }
2827
+
2828
+ throw error;
2829
+ }
2830
+ }
2831
+
2832
+ async agencyHosting_uploadFile(filePath, relativePath, uploadUrl, authRestToken, authToken) {
2833
+ return new Promise(async (resolve, reject) => {
2834
+ try {
2835
+ const stats = fs.statSync(filePath);
2836
+ const fileStream = fs.createReadStream(filePath);
2837
+
2838
+ const cleanUploadUrl = uploadUrl.replace(new RegExp('/$'), '');
2839
+ const normalizedPath = this.agencyHosting_normalizePath(relativePath);
2840
+ const uploadUrlWithFile = `${cleanUploadUrl}/${normalizedPath}?override=true`;
2841
+
2842
+ const requestHeaders = {
2843
+ 'X-Auth': authToken,
2844
+ 'X-Auth-Rest': authRestToken,
2845
+ 'upload-length': stats.size.toString(),
2846
+ 'upload-offset': '0'
2847
+ };
2848
+
2849
+ try {
2850
+ this.log('debug', `Making pre-upload POST request to ${uploadUrlWithFile}`);
2851
+ await axios.post(uploadUrlWithFile, '', {
2852
+ headers: requestHeaders,
2853
+ timeout: 60000, // 60s
2854
+ validateStatus: function (status) {
2855
+ return status == 201;
2856
+ }
2857
+ });
2858
+ } catch (error) {
2859
+ const errorMessage = error instanceof Error ? error.message : String(error);
2860
+
2861
+ if (axios.isAxiosError(error)) {
2862
+ const responseData = error.response?.data;
2863
+ const responseStatus = error.response?.status;
2864
+ const responseHeaders = error.response?.headers;
2865
+ const responseText = typeof responseData === 'object' ? JSON.stringify(responseData) : responseData;
2866
+
2867
+ this.log('error', 'Pre-upload POST request failed - Full Response Details:', {
2868
+ status: responseStatus,
2869
+ headers: responseHeaders,
2870
+ data: responseText,
2871
+ message: errorMessage
2872
+ });
2873
+ reject(new Error(`Pre-upload request failed: ${errorMessage}`));
2874
+ return;
2875
+ } else {
2876
+ this.log('error', `Pre-upload POST request failed: ${errorMessage}`);
2877
+ reject(new Error(`Pre-upload request failed: ${errorMessage}`));
2878
+ return;
2879
+ }
2880
+ }
2881
+
2882
+ const upload = new tus.Upload(fileStream, {
2883
+ uploadUrl: uploadUrlWithFile,
2884
+ retryDelays: [1000, 2000, 4000, 8000, 16000, 20000],
2885
+ uploadDataDuringCreation: false,
2886
+ parallelUploads: 1,
2887
+ chunkSize: 10485760,
2888
+ headers: requestHeaders,
2889
+ removeFingerprintOnSuccess: true,
2890
+ uploadSize: stats.size,
2891
+ metadata: {
2892
+ filename: path.basename(relativePath)
2893
+ },
2894
+ onError: (error) => {
2895
+ this.log('error', `TUS upload error for ${relativePath}`, { error: error.message });
2896
+ reject(new Error(`Upload failed: ${error.message}`));
2897
+ },
2898
+ onSuccess: () => {
2899
+ this.log('info', `TUS upload completed for ${relativePath}`, { url: upload.url });
2900
+ resolve({
2901
+ url: upload.url || uploadUrlWithFile,
2902
+ filename: relativePath
2903
+ });
2904
+ }
2905
+ });
2906
+
2907
+ upload.start();
2908
+
2909
+ } catch (error) {
2910
+ const errorMessage = error instanceof Error ? error.message : String(error);
2911
+ this.log('error', `Error preparing upload for ${filePath}`, { error: errorMessage });
2912
+ reject(new Error(`Failed to prepare upload: ${errorMessage}`));
2913
+ }
2914
+ });
2915
+ }
2916
+
2917
+ async agencyHosting_deployNodeStatic_triggerBuildAssets(websiteUid, archivePath) {
2918
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
2919
+ const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/build-assets`, baseUrl).toString();
2920
+
2921
+ try {
2922
+ const bearerToken = await this.getAuthToken();
2923
+
2148
2924
  const config = {
2149
2925
  method: 'post',
2150
2926
  url,
@@ -2153,7 +2929,9 @@ class MCPServer {
2153
2929
  'Authorization': `Bearer ${bearerToken}`,
2154
2930
  'Content-Type': 'application/json'
2155
2931
  },
2156
- data: deployData,
2932
+ data: {
2933
+ archive_path: archivePath
2934
+ },
2157
2935
  timeout: 60000,
2158
2936
  validateStatus: function (status) {
2159
2937
  return status < 500;
@@ -2162,16 +2940,16 @@ class MCPServer {
2162
2940
 
2163
2941
  const response = await axios(config);
2164
2942
 
2165
- if (response.status !== 200) {
2943
+ if (response.status < 200 || response.status >= 300) {
2166
2944
  throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
2167
2945
  }
2168
2946
 
2169
- this.log('info', `Successfully triggered deployment for ${domain}`);
2947
+ this.log('info', `Successfully triggered build for website ${websiteUid}`);
2170
2948
  return response.data;
2171
2949
 
2172
2950
  } catch (error) {
2173
2951
  const errorMessage = error instanceof Error ? error.message : String(error);
2174
- this.log('error', `Failed to trigger deployment: ${errorMessage}`);
2952
+ this.log('error', `Failed to trigger build: ${errorMessage}`);
2175
2953
 
2176
2954
  if (axios.isAxiosError(error)) {
2177
2955
  const responseData = error.response?.data;
@@ -2186,20 +2964,20 @@ class MCPServer {
2186
2964
  }
2187
2965
  }
2188
2966
 
2189
- async handleStaticWebsiteDeploy(params) {
2190
- const { domain, archivePath, removeArchive = false } = params;
2191
-
2192
- this.hosting_deployStaticWebsite_validateRequiredParams(params);
2193
- this.hosting_deployStaticWebsite_validateArchiveFile(archivePath);
2967
+ async handleNodeStaticDeploy(params) {
2968
+ const { domain, archivePath, removeArchive = true } = params;
2194
2969
 
2195
- this.log('info', `Resolving username from domain: ${domain}`);
2196
- const username = await this.resolveUsername(domain);
2970
+ this.agencyHosting_validateRequiredParams(params);
2971
+ this.agencyHosting_validateArchiveFile(archivePath);
2972
+
2973
+ this.log('info', `Resolving website UID from domain: ${domain}`);
2974
+ const websiteUid = await this.agencyHosting_resolveWebsiteUid(domain);
2197
2975
 
2198
2976
  this.log('info', `Starting archive upload for ${domain}`);
2199
-
2977
+
2200
2978
  let uploadCredentials;
2201
2979
  try {
2202
- uploadCredentials = await this.fetchUploadCredentials(username, domain);
2980
+ uploadCredentials = await this.agencyHosting_fetchUploadCredentials(websiteUid);
2203
2981
  } catch (error) {
2204
2982
  const errorMessage = error instanceof Error ? error.message : String(error);
2205
2983
  throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
@@ -2212,12 +2990,12 @@ class MCPServer {
2212
2990
  }
2213
2991
 
2214
2992
  const archiveBasename = path.basename(archivePath);
2993
+ const uploadDir = `.h5g/${this.agencyHosting_generateUploadDir()}`;
2215
2994
  let uploadResult;
2216
2995
  try {
2217
- const stats = fs.statSync(archivePath);
2218
- uploadResult = await this.uploadFile(
2996
+ uploadResult = await this.agencyHosting_uploadFile(
2219
2997
  archivePath,
2220
- archiveBasename,
2998
+ `${uploadDir}/${archiveBasename}`,
2221
2999
  uploadUrl,
2222
3000
  authRestToken,
2223
3001
  authToken
@@ -2231,12 +3009,12 @@ class MCPServer {
2231
3009
 
2232
3010
  let deployResult;
2233
3011
  try {
2234
- this.log('info', `Triggering deployment for ${domain}`);
2235
- deployResult = await this.hosting_deployStaticWebsite_triggerDeploy(username, domain, archivePath);
3012
+ this.log('info', `Triggering build for ${domain}`);
3013
+ deployResult = await this.agencyHosting_deployNodeStatic_triggerBuildAssets(websiteUid, uploadDir);
2236
3014
  } catch (error) {
2237
3015
  const errorMessage = error instanceof Error ? error.message : String(error);
2238
- this.log('error', `Failed to trigger deployment: ${errorMessage}`);
2239
- const archiveRemoved = this.hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive);
3016
+ this.log('error', `Failed to trigger build: ${errorMessage}`);
3017
+ const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
2240
3018
 
2241
3019
  return {
2242
3020
  upload: {
@@ -2255,7 +3033,7 @@ class MCPServer {
2255
3033
  };
2256
3034
  }
2257
3035
 
2258
- const archiveRemoved = this.hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive);
3036
+ const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
2259
3037
 
2260
3038
  return {
2261
3039
  upload: {
@@ -2274,52 +3052,25 @@ class MCPServer {
2274
3052
  };
2275
3053
  }
2276
3054
 
2277
- hosting_listJsDeployments_validateRequiredParams(params) {
2278
- const { domain } = params;
2279
-
2280
- if (!domain || typeof domain !== 'string') {
2281
- throw new Error('domain is required and must be a string');
2282
- }
2283
- }
2284
-
2285
- hosting_listJsDeployments_buildQueryParams(params) {
2286
- const { page, perPage, states } = params;
2287
- const queryParams = new URLSearchParams();
2288
-
2289
- if (page !== undefined && page !== null) {
2290
- queryParams.append('page', page.toString());
2291
- }
2292
-
2293
- if (perPage !== undefined && perPage !== null) {
2294
- queryParams.append('per_page', perPage.toString());
2295
- }
2296
-
2297
- if (states && Array.isArray(states) && states.length > 0) {
2298
- states.forEach(state => {
2299
- queryParams.append('states[]', state);
2300
- });
2301
- }
2302
-
2303
- return queryParams.toString();
2304
- }
2305
-
2306
- async hosting_listJsDeployments_fetchDeployments(username, domain, queryParams) {
3055
+ async agencyHosting_deployPhpApp_triggerImportArchive(websiteUid, archiveName) {
2307
3056
  const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
2308
- const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
2309
-
2310
- const fullUrl = queryParams ? `${url}?${queryParams}` : url;
3057
+ const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/files/import-archive`, baseUrl).toString();
2311
3058
 
2312
3059
  try {
2313
3060
  const bearerToken = await this.getAuthToken();
2314
3061
 
2315
3062
  const config = {
2316
- method: 'get',
2317
- url: fullUrl,
3063
+ method: 'post',
3064
+ url,
2318
3065
  headers: {
2319
3066
  ...this.headers,
2320
- 'Authorization': `Bearer ${bearerToken}`
3067
+ 'Authorization': `Bearer ${bearerToken}`,
3068
+ 'Content-Type': 'application/json'
2321
3069
  },
2322
- timeout: 60000, // 60s
3070
+ data: {
3071
+ archive_name: archiveName
3072
+ },
3073
+ timeout: 60000,
2323
3074
  validateStatus: function (status) {
2324
3075
  return status < 500;
2325
3076
  }
@@ -2327,16 +3078,16 @@ class MCPServer {
2327
3078
 
2328
3079
  const response = await axios(config);
2329
3080
 
2330
- if (response.status !== 200) {
3081
+ if (response.status < 200 || response.status >= 300) {
2331
3082
  throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
2332
3083
  }
2333
3084
 
2334
- this.log('info', `Successfully fetched deployments for ${domain}`);
3085
+ this.log('info', `Successfully triggered import for website ${websiteUid}`);
2335
3086
  return response.data;
2336
3087
 
2337
3088
  } catch (error) {
2338
3089
  const errorMessage = error instanceof Error ? error.message : String(error);
2339
- this.log('error', `Failed to fetch deployments: ${errorMessage}`);
3090
+ this.log('error', `Failed to trigger import: ${errorMessage}`);
2340
3091
 
2341
3092
  if (axios.isAxiosError(error)) {
2342
3093
  const responseData = error.response?.data;
@@ -2351,144 +3102,90 @@ class MCPServer {
2351
3102
  }
2352
3103
  }
2353
3104
 
2354
- async handleListJavascriptDeployments(params) {
2355
- const { domain, page, perPage, states } = params;
3105
+ async handlePhpAppDeploy(params) {
3106
+ const { domain, archivePath, removeArchive = true } = params;
2356
3107
 
2357
- this.hosting_listJsDeployments_validateRequiredParams(params);
3108
+ this.agencyHosting_validateRequiredParams(params);
3109
+ this.agencyHosting_validateArchiveFile(archivePath);
2358
3110
 
2359
- // Auto-resolve username from domain
2360
- this.log('info', `Resolving username from domain: ${domain}`);
2361
- const username = await this.resolveUsername(domain);
3111
+ this.log('info', `Resolving website UID from domain: ${domain}`);
3112
+ const websiteUid = await this.agencyHosting_resolveWebsiteUid(domain);
2362
3113
 
2363
- // Build query parameters
2364
- const queryParams = this.hosting_listJsDeployments_buildQueryParams(params);
3114
+ this.log('info', `Starting archive upload for ${domain}`);
2365
3115
 
2366
- // Fetch deployments
2367
- let deployments;
3116
+ let uploadCredentials;
2368
3117
  try {
2369
- this.log('info', `Fetching deployments for ${domain}`);
2370
- deployments = await this.hosting_listJsDeployments_fetchDeployments(username, domain, queryParams);
3118
+ uploadCredentials = await this.agencyHosting_fetchUploadCredentials(websiteUid);
2371
3119
  } catch (error) {
2372
3120
  const errorMessage = error instanceof Error ? error.message : String(error);
2373
- this.log('error', `Failed to fetch deployments: ${errorMessage}`);
2374
- throw error;
2375
- }
2376
-
2377
- return {
2378
- status: 'success',
2379
- domain,
2380
- username,
2381
- queryParams: {
2382
- page,
2383
- perPage,
2384
- states
2385
- },
2386
- deployments
2387
- };
2388
- }
2389
-
2390
- hosting_showJsDeploymentLogs_validateRequiredParams(params) {
2391
- const { domain, buildUuid, fromLine } = params;
2392
-
2393
- if (!domain || typeof domain !== 'string') {
2394
- throw new Error('domain is required and must be a string');
3121
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
2395
3122
  }
2396
3123
 
2397
- if (!buildUuid || typeof buildUuid !== 'string') {
2398
- throw new Error('buildUuid is required and must be a string');
2399
- }
3124
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
2400
3125
 
2401
- if (fromLine !== undefined && (typeof fromLine !== 'number' || !Number.isInteger(fromLine) || fromLine < 0)) {
2402
- throw new Error('fromLine must be a non-negative integer when provided');
3126
+ if (!uploadUrl || !authToken || !authRestToken) {
3127
+ throw new Error('Invalid upload credentials received from API');
2403
3128
  }
2404
- }
2405
-
2406
- hosting_showJsDeploymentLogs_buildQueryParams(params) {
2407
- const { fromLine } = params;
2408
- const queryParams = new URLSearchParams();
2409
-
2410
- const line = (typeof fromLine === 'number' && Number.isInteger(fromLine) && fromLine >= 0) ? fromLine : 0;
2411
- queryParams.append('from_line', line.toString());
2412
-
2413
- return queryParams.toString();
2414
- }
2415
-
2416
- async hosting_showJsDeploymentLogs_fetchLogs(username, domain, buildUuid, queryParams) {
2417
- const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
2418
- const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/${buildUuid}/logs`, baseUrl).toString();
2419
-
2420
- const fullUrl = queryParams ? `${url}?${queryParams}` : url;
2421
3129
 
3130
+ const archiveBasename = path.basename(archivePath);
3131
+ let uploadResult;
2422
3132
  try {
2423
- const bearerToken = await this.getAuthToken();
2424
-
2425
- const config = {
2426
- method: 'get',
2427
- url: fullUrl,
2428
- headers: {
2429
- ...this.headers,
2430
- 'Authorization': `Bearer ${bearerToken}`
2431
- },
2432
- timeout: 60000,
2433
- validateStatus: function (status) {
2434
- return status < 500;
2435
- }
2436
- };
2437
-
2438
- const response = await axios(config);
2439
-
2440
- if (response.status !== 200) {
2441
- throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
2442
- }
2443
-
2444
- this.log('info', `Successfully fetched logs for ${domain} build ${buildUuid}`);
2445
- return response.data;
3133
+ uploadResult = await this.agencyHosting_uploadFile(
3134
+ archivePath,
3135
+ `.h5g/${archiveBasename}`,
3136
+ uploadUrl,
3137
+ authRestToken,
3138
+ authToken
3139
+ );
2446
3140
 
3141
+ this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
2447
3142
  } catch (error) {
2448
3143
  const errorMessage = error instanceof Error ? error.message : String(error);
2449
- this.log('error', `Failed to fetch logs: ${errorMessage}`);
2450
-
2451
- if (axios.isAxiosError(error)) {
2452
- const responseData = error.response?.data;
2453
- const responseStatus = error.response?.status;
2454
- this.log('error', 'API Error Details:', {
2455
- status: responseStatus,
2456
- data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
2457
- });
2458
- }
2459
-
2460
- throw error;
3144
+ throw new Error(`Failed to upload archive: ${errorMessage}`);
2461
3145
  }
2462
- }
2463
-
2464
- async handleShowJsDeploymentLogs(params) {
2465
- const { domain, buildUuid, fromLine } = params;
2466
-
2467
- this.hosting_showJsDeploymentLogs_validateRequiredParams(params);
2468
-
2469
- this.log('info', `Resolving username from domain: ${domain}`);
2470
- const username = await this.resolveUsername(domain);
2471
3146
 
2472
- const queryParams = this.hosting_showJsDeploymentLogs_buildQueryParams(params);
2473
-
2474
- let logs;
3147
+ let deployResult;
2475
3148
  try {
2476
- this.log('info', `Fetching logs for ${domain}, build ${buildUuid}`);
2477
- logs = await this.hosting_showJsDeploymentLogs_fetchLogs(username, domain, buildUuid, queryParams);
3149
+ this.log('info', `Triggering import for ${domain}`);
3150
+ deployResult = await this.agencyHosting_deployPhpApp_triggerImportArchive(websiteUid, archiveBasename);
2478
3151
  } catch (error) {
2479
3152
  const errorMessage = error instanceof Error ? error.message : String(error);
2480
- this.log('error', `Failed to fetch logs: ${errorMessage}`);
2481
- throw error;
3153
+ this.log('error', `Failed to trigger import: ${errorMessage}`);
3154
+ const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
3155
+
3156
+ return {
3157
+ upload: {
3158
+ status: 'success',
3159
+ data: {
3160
+ filename: uploadResult.filename
3161
+ }
3162
+ },
3163
+ deploy: {
3164
+ status: 'error',
3165
+ error: errorMessage
3166
+ },
3167
+ removeArchive: {
3168
+ status: archiveRemoved ? 'success' : 'skipped'
3169
+ }
3170
+ };
2482
3171
  }
2483
3172
 
2484
- const effectiveFromLine = (typeof fromLine === 'number' && Number.isInteger(fromLine) && fromLine >= 0) ? fromLine : 0;
3173
+ const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
2485
3174
 
2486
3175
  return {
2487
- domain,
2488
- username,
2489
- buildUuid,
2490
- fromLine: effectiveFromLine,
2491
- logs
3176
+ upload: {
3177
+ status: 'success',
3178
+ data: {
3179
+ filename: uploadResult.filename
3180
+ }
3181
+ },
3182
+ deploy: {
3183
+ status: 'success',
3184
+ data: deployResult
3185
+ },
3186
+ removeArchive: {
3187
+ status: archiveRemoved ? 'success' : 'skipped'
3188
+ }
2492
3189
  };
2493
3190
  }
2494
3191