hostinger-api-mcp 1.6.1 → 1.7.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.
@@ -227,6 +227,10 @@ class MCPServer {
227
227
 
228
228
  private async executeCustomTool(tool: OpenApiTool, params: Record<string, any>): Promise<any> {
229
229
  switch (tool.name) {
230
+ case 'agencyHosting_deployNodeStaticWebsite':
231
+ return await this.handleNodeStaticDeploy(params);
232
+ case 'agencyHosting_deployPhpApplication':
233
+ return await this.handlePhpAppDeploy(params);
230
234
  case 'hosting_importWordpressWebsite':
231
235
  return await this.handleWordpressWebsiteImport(params);
232
236
  case 'hosting_deployWordpressPlugin':
@@ -246,6 +250,541 @@ class MCPServer {
246
250
  }
247
251
  }
248
252
 
253
+ private agencyHosting_normalizePath(pathString: string): string {
254
+ return pathString.replace(/\\/g, '/');
255
+ }
256
+
257
+ private agencyHosting_generateUploadDir(): string {
258
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
259
+ let result = '';
260
+ for (let i = 0; i < 12; i++) {
261
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
262
+ }
263
+ return result;
264
+ }
265
+
266
+ private agencyHosting_validateRequiredParams(params: Record<string, any>): void {
267
+ const { domain, archivePath, removeArchive } = params;
268
+
269
+ if (!domain || typeof domain !== 'string') {
270
+ throw new Error('domain is required and must be a string');
271
+ }
272
+
273
+ if (!archivePath || typeof archivePath !== 'string') {
274
+ throw new Error('archivePath is required and must be a string');
275
+ }
276
+
277
+ if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
278
+ throw new Error('removeArchive must be a boolean if provided');
279
+ }
280
+ }
281
+
282
+ private agencyHosting_validateArchiveFile(archivePath: string): void {
283
+ if (!fs.existsSync(archivePath)) {
284
+ throw new Error(`Archive file not found: ${archivePath}`);
285
+ }
286
+
287
+ const archiveStats = fs.statSync(archivePath);
288
+ if (!archiveStats.isFile()) {
289
+ throw new Error(`Archive path is not a file: ${archivePath}`);
290
+ }
291
+ }
292
+
293
+ private agencyHosting_removeArchive(archivePath: string, removeArchive: boolean): boolean {
294
+ if (!removeArchive) {
295
+ return false;
296
+ }
297
+
298
+ try {
299
+ this.log('info', `Removing archive file: ${archivePath}`);
300
+ fs.unlinkSync(archivePath);
301
+ this.log('info', `Successfully removed archive file: ${archivePath}`);
302
+ return true;
303
+ } catch (error) {
304
+ const errorMessage = error instanceof Error ? error.message : String(error);
305
+ this.log('error', `Failed to remove archive file: ${errorMessage}`);
306
+ return false;
307
+ }
308
+ }
309
+
310
+ private async agencyHosting_resolveWebsiteUid(domain: string): Promise<string> {
311
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
312
+ const perPage = 100;
313
+
314
+ try {
315
+ const bearerToken = await this.getAuthToken();
316
+ let page = 1;
317
+ let total = 0;
318
+
319
+ do {
320
+ const url = new URL(
321
+ `api/agency-hosting/v1/domains?page=${page}&per_page=${perPage}`,
322
+ baseUrl
323
+ ).toString();
324
+
325
+ const config: AxiosRequestConfig = {
326
+ method: 'get',
327
+ url,
328
+ headers: {
329
+ ...this.headers,
330
+ 'Authorization': `Bearer ${bearerToken}`
331
+ },
332
+ timeout: 60000, // 60s
333
+ validateStatus: function (status: number): boolean {
334
+ return status < 500;
335
+ }
336
+ };
337
+
338
+ const response = await axios(config);
339
+
340
+ if (response.status !== 200) {
341
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
342
+ }
343
+
344
+ const domains = response.data?.data ?? [];
345
+ const match = domains.find((item: any) => item.fqdn === domain);
346
+ if (match?.website_uid) {
347
+ this.log('info', `Resolved website UID: ${match.website_uid} for domain: ${domain}`);
348
+ return match.website_uid;
349
+ }
350
+
351
+ total = response.data?.meta?.total ?? domains.length;
352
+ page++;
353
+ } while ((page - 1) * perPage < total);
354
+
355
+ throw new Error(`No Agency Plan website found for domain: ${domain}`);
356
+
357
+ } catch (error) {
358
+ const errorMessage = error instanceof Error ? error.message : String(error);
359
+ this.log('error', `Failed to resolve website UID for domain ${domain}: ${errorMessage}`);
360
+
361
+ if (axios.isAxiosError(error)) {
362
+ const responseData = error.response?.data;
363
+ const responseStatus = error.response?.status;
364
+ this.log('error', 'API Error Details:', {
365
+ status: responseStatus,
366
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
367
+ });
368
+ }
369
+
370
+ throw error;
371
+ }
372
+ }
373
+
374
+ private async agencyHosting_fetchUploadCredentials(
375
+ websiteUid: string
376
+ ): Promise<{ url: string; auth_key: string; rest_auth_key: string }> {
377
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
378
+ const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/files/upload-urls`, baseUrl).toString();
379
+
380
+ try {
381
+ const bearerToken = await this.getAuthToken();
382
+
383
+ const config: AxiosRequestConfig = {
384
+ method: 'post',
385
+ url,
386
+ headers: {
387
+ ...this.headers,
388
+ 'Authorization': `Bearer ${bearerToken}`,
389
+ 'Content-Type': 'application/json'
390
+ },
391
+ timeout: 60000, // 60s
392
+ validateStatus: function (status: number): boolean {
393
+ return status < 500;
394
+ }
395
+ };
396
+
397
+ const response = await axios(config);
398
+
399
+ if (response.status !== 200) {
400
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
401
+ }
402
+
403
+ return response.data;
404
+
405
+ } catch (error) {
406
+ const errorMessage = error instanceof Error ? error.message : String(error);
407
+ this.log('error', `Failed to fetch upload credentials: ${errorMessage}`);
408
+
409
+ if (axios.isAxiosError(error)) {
410
+ const responseData = error.response?.data;
411
+ const responseStatus = error.response?.status;
412
+ this.log('error', 'API Error Details:', {
413
+ status: responseStatus,
414
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
415
+ });
416
+ }
417
+
418
+ throw error;
419
+ }
420
+ }
421
+
422
+ private async agencyHosting_uploadFile(
423
+ filePath: string,
424
+ relativePath: string,
425
+ uploadUrl: string,
426
+ authRestToken: string,
427
+ authToken: string
428
+ ): Promise<{ url: string; filename: string }> {
429
+ return new Promise(async (resolve, reject) => {
430
+ try {
431
+ const stats = fs.statSync(filePath);
432
+ const fileStream = fs.createReadStream(filePath);
433
+
434
+ const cleanUploadUrl = uploadUrl.replace(new RegExp('/$'), '');
435
+ const normalizedPath = this.agencyHosting_normalizePath(relativePath);
436
+ const uploadUrlWithFile = `${cleanUploadUrl}/${normalizedPath}?override=true`;
437
+
438
+ const requestHeaders: Record<string, string> = {
439
+ 'X-Auth': authToken,
440
+ 'X-Auth-Rest': authRestToken,
441
+ 'upload-length': stats.size.toString(),
442
+ 'upload-offset': '0'
443
+ };
444
+
445
+ try {
446
+ this.log('debug', `Making pre-upload POST request to ${uploadUrlWithFile}`);
447
+ await axios.post(uploadUrlWithFile, '', {
448
+ headers: requestHeaders,
449
+ timeout: 60000, // 60s
450
+ validateStatus: function (status: number): boolean {
451
+ return status == 201;
452
+ }
453
+ });
454
+ } catch (error) {
455
+ const errorMessage = error instanceof Error ? error.message : String(error);
456
+
457
+ if (axios.isAxiosError(error)) {
458
+ const responseData = error.response?.data;
459
+ const responseStatus = error.response?.status;
460
+ const responseHeaders = error.response?.headers;
461
+ const responseText = typeof responseData === 'object' ? JSON.stringify(responseData) : responseData;
462
+
463
+ this.log('error', 'Pre-upload POST request failed - Full Response Details:', {
464
+ status: responseStatus,
465
+ headers: responseHeaders,
466
+ data: responseText,
467
+ message: errorMessage
468
+ });
469
+ reject(new Error(`Pre-upload request failed: ${errorMessage}`));
470
+ return;
471
+ } else {
472
+ this.log('error', `Pre-upload POST request failed: ${errorMessage}`);
473
+ reject(new Error(`Pre-upload request failed: ${errorMessage}`));
474
+ return;
475
+ }
476
+ }
477
+
478
+ const upload = new tus.Upload(fileStream, {
479
+ uploadUrl: uploadUrlWithFile,
480
+ retryDelays: [1000, 2000, 4000, 8000, 16000, 20000],
481
+ uploadDataDuringCreation: false,
482
+ parallelUploads: 1,
483
+ chunkSize: 10485760,
484
+ headers: requestHeaders,
485
+ removeFingerprintOnSuccess: true,
486
+ uploadSize: stats.size,
487
+ metadata: {
488
+ filename: path.basename(relativePath)
489
+ },
490
+ onError: (error: Error) => {
491
+ this.log('error', `TUS upload error for ${relativePath}`, { error: error.message });
492
+ reject(new Error(`Upload failed: ${error.message}`));
493
+ },
494
+ onSuccess: () => {
495
+ this.log('info', `TUS upload completed for ${relativePath}`, { url: upload.url });
496
+ resolve({
497
+ url: upload.url || uploadUrlWithFile,
498
+ filename: relativePath
499
+ });
500
+ }
501
+ });
502
+
503
+ upload.start();
504
+
505
+ } catch (error) {
506
+ const errorMessage = error instanceof Error ? error.message : String(error);
507
+ this.log('error', `Error preparing upload for ${filePath}`, { error: errorMessage });
508
+ reject(new Error(`Failed to prepare upload: ${errorMessage}`));
509
+ }
510
+ });
511
+ }
512
+
513
+ private async agencyHosting_deployNodeStatic_triggerBuildAssets(websiteUid: string, archivePath: string): Promise<any> {
514
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
515
+ const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/build-assets`, baseUrl).toString();
516
+
517
+ try {
518
+ const bearerToken = await this.getAuthToken();
519
+
520
+ const config: AxiosRequestConfig = {
521
+ method: 'post',
522
+ url,
523
+ headers: {
524
+ ...this.headers,
525
+ 'Authorization': `Bearer ${bearerToken}`,
526
+ 'Content-Type': 'application/json'
527
+ },
528
+ data: {
529
+ archive_path: archivePath
530
+ },
531
+ timeout: 60000,
532
+ validateStatus: function (status: number): boolean {
533
+ return status < 500;
534
+ }
535
+ };
536
+
537
+ const response = await axios(config);
538
+
539
+ if (response.status < 200 || response.status >= 300) {
540
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
541
+ }
542
+
543
+ this.log('info', `Successfully triggered build for website ${websiteUid}`);
544
+ return response.data;
545
+
546
+ } catch (error) {
547
+ const errorMessage = error instanceof Error ? error.message : String(error);
548
+ this.log('error', `Failed to trigger build: ${errorMessage}`);
549
+
550
+ if (axios.isAxiosError(error)) {
551
+ const responseData = error.response?.data;
552
+ const responseStatus = error.response?.status;
553
+ this.log('error', 'API Error Details:', {
554
+ status: responseStatus,
555
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
556
+ });
557
+ }
558
+
559
+ throw error;
560
+ }
561
+ }
562
+
563
+ private async handleNodeStaticDeploy(params: Record<string, any>): Promise<any> {
564
+ const { domain, archivePath, removeArchive = true } = params;
565
+
566
+ this.agencyHosting_validateRequiredParams(params);
567
+ this.agencyHosting_validateArchiveFile(archivePath);
568
+
569
+ this.log('info', `Resolving website UID from domain: ${domain}`);
570
+ const websiteUid = await this.agencyHosting_resolveWebsiteUid(domain);
571
+
572
+ this.log('info', `Starting archive upload for ${domain}`);
573
+
574
+ let uploadCredentials: any;
575
+ try {
576
+ uploadCredentials = await this.agencyHosting_fetchUploadCredentials(websiteUid);
577
+ } catch (error) {
578
+ const errorMessage = error instanceof Error ? error.message : String(error);
579
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
580
+ }
581
+
582
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
583
+
584
+ if (!uploadUrl || !authToken || !authRestToken) {
585
+ throw new Error('Invalid upload credentials received from API');
586
+ }
587
+
588
+ const archiveBasename = path.basename(archivePath);
589
+ const uploadDir = `.h5g/${this.agencyHosting_generateUploadDir()}`;
590
+ let uploadResult: any;
591
+ try {
592
+ uploadResult = await this.agencyHosting_uploadFile(
593
+ archivePath,
594
+ `${uploadDir}/${archiveBasename}`,
595
+ uploadUrl,
596
+ authRestToken,
597
+ authToken
598
+ );
599
+
600
+ this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
601
+ } catch (error) {
602
+ const errorMessage = error instanceof Error ? error.message : String(error);
603
+ throw new Error(`Failed to upload archive: ${errorMessage}`);
604
+ }
605
+
606
+ let deployResult: any;
607
+ try {
608
+ this.log('info', `Triggering build for ${domain}`);
609
+ deployResult = await this.agencyHosting_deployNodeStatic_triggerBuildAssets(websiteUid, uploadDir);
610
+ } catch (error) {
611
+ const errorMessage = error instanceof Error ? error.message : String(error);
612
+ this.log('error', `Failed to trigger build: ${errorMessage}`);
613
+ const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
614
+
615
+ return {
616
+ upload: {
617
+ status: 'success',
618
+ data: {
619
+ filename: uploadResult.filename
620
+ }
621
+ },
622
+ deploy: {
623
+ status: 'error',
624
+ error: errorMessage
625
+ },
626
+ removeArchive: {
627
+ status: archiveRemoved ? 'success' : 'skipped'
628
+ }
629
+ };
630
+ }
631
+
632
+ const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
633
+
634
+ return {
635
+ upload: {
636
+ status: 'success',
637
+ data: {
638
+ filename: uploadResult.filename
639
+ }
640
+ },
641
+ deploy: {
642
+ status: 'success',
643
+ data: deployResult
644
+ },
645
+ removeArchive: {
646
+ status: archiveRemoved ? 'success' : 'skipped'
647
+ }
648
+ };
649
+ }
650
+
651
+ private async agencyHosting_deployPhpApp_triggerImportArchive(websiteUid: string, archiveName: string): Promise<any> {
652
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
653
+ const url = new URL(`api/agency-hosting/v1/websites/${websiteUid}/files/import-archive`, baseUrl).toString();
654
+
655
+ try {
656
+ const bearerToken = await this.getAuthToken();
657
+
658
+ const config: AxiosRequestConfig = {
659
+ method: 'post',
660
+ url,
661
+ headers: {
662
+ ...this.headers,
663
+ 'Authorization': `Bearer ${bearerToken}`,
664
+ 'Content-Type': 'application/json'
665
+ },
666
+ data: {
667
+ archive_name: archiveName
668
+ },
669
+ timeout: 60000,
670
+ validateStatus: function (status: number): boolean {
671
+ return status < 500;
672
+ }
673
+ };
674
+
675
+ const response = await axios(config);
676
+
677
+ if (response.status < 200 || response.status >= 300) {
678
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
679
+ }
680
+
681
+ this.log('info', `Successfully triggered import for website ${websiteUid}`);
682
+ return response.data;
683
+
684
+ } catch (error) {
685
+ const errorMessage = error instanceof Error ? error.message : String(error);
686
+ this.log('error', `Failed to trigger import: ${errorMessage}`);
687
+
688
+ if (axios.isAxiosError(error)) {
689
+ const responseData = error.response?.data;
690
+ const responseStatus = error.response?.status;
691
+ this.log('error', 'API Error Details:', {
692
+ status: responseStatus,
693
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
694
+ });
695
+ }
696
+
697
+ throw error;
698
+ }
699
+ }
700
+
701
+ private async handlePhpAppDeploy(params: Record<string, any>): Promise<any> {
702
+ const { domain, archivePath, removeArchive = true } = params;
703
+
704
+ this.agencyHosting_validateRequiredParams(params);
705
+ this.agencyHosting_validateArchiveFile(archivePath);
706
+
707
+ this.log('info', `Resolving website UID from domain: ${domain}`);
708
+ const websiteUid = await this.agencyHosting_resolveWebsiteUid(domain);
709
+
710
+ this.log('info', `Starting archive upload for ${domain}`);
711
+
712
+ let uploadCredentials: any;
713
+ try {
714
+ uploadCredentials = await this.agencyHosting_fetchUploadCredentials(websiteUid);
715
+ } catch (error) {
716
+ const errorMessage = error instanceof Error ? error.message : String(error);
717
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
718
+ }
719
+
720
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
721
+
722
+ if (!uploadUrl || !authToken || !authRestToken) {
723
+ throw new Error('Invalid upload credentials received from API');
724
+ }
725
+
726
+ const archiveBasename = path.basename(archivePath);
727
+ let uploadResult: any;
728
+ try {
729
+ uploadResult = await this.agencyHosting_uploadFile(
730
+ archivePath,
731
+ `.h5g/${archiveBasename}`,
732
+ uploadUrl,
733
+ authRestToken,
734
+ authToken
735
+ );
736
+
737
+ this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
738
+ } catch (error) {
739
+ const errorMessage = error instanceof Error ? error.message : String(error);
740
+ throw new Error(`Failed to upload archive: ${errorMessage}`);
741
+ }
742
+
743
+ let deployResult: any;
744
+ try {
745
+ this.log('info', `Triggering import for ${domain}`);
746
+ deployResult = await this.agencyHosting_deployPhpApp_triggerImportArchive(websiteUid, archiveBasename);
747
+ } catch (error) {
748
+ const errorMessage = error instanceof Error ? error.message : String(error);
749
+ this.log('error', `Failed to trigger import: ${errorMessage}`);
750
+ const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
751
+
752
+ return {
753
+ upload: {
754
+ status: 'success',
755
+ data: {
756
+ filename: uploadResult.filename
757
+ }
758
+ },
759
+ deploy: {
760
+ status: 'error',
761
+ error: errorMessage
762
+ },
763
+ removeArchive: {
764
+ status: archiveRemoved ? 'success' : 'skipped'
765
+ }
766
+ };
767
+ }
768
+
769
+ const archiveRemoved = this.agencyHosting_removeArchive(archivePath, removeArchive);
770
+
771
+ return {
772
+ upload: {
773
+ status: 'success',
774
+ data: {
775
+ filename: uploadResult.filename
776
+ }
777
+ },
778
+ deploy: {
779
+ status: 'success',
780
+ data: deployResult
781
+ },
782
+ removeArchive: {
783
+ status: archiveRemoved ? 'success' : 'skipped'
784
+ }
785
+ };
786
+ }
787
+
249
788
  private normalizePath(pathString: string): string {
250
789
  return pathString.replace(/\\/g, '/');
251
790
  }
@@ -2059,7 +2598,7 @@ class MCPServer {
2059
2598
  });
2060
2599
 
2061
2600
  // Set up CORS for all routes
2062
- app.options("*", (req, res) => {
2601
+ app.options("/{*splat}", (req, res) => {
2063
2602
  res.header("Access-Control-Allow-Origin", "*");
2064
2603
  res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
2065
2604
  res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, x-session-id");
@@ -1,5 +1,73 @@
1
1
  // Auto-generated tool list for group: agency-hosting
2
2
  export default [
3
+ {
4
+ "name": "agencyHosting_deployNodeStaticWebsite",
5
+ "topic": "agency-hosting",
6
+ "description": "Deploy a node-static Agency Plan (h5g) website from an archive file. WARNING: this overwrites the website's existing contents and cannot be undone — always confirm with the user before proceeding. Use this for Agency Plan websites of type node-static (a Node.js-built static site that requires a build step or a plain simple static site). The tool resolves the website from its domain, uploads the archive to the website's file browser over TUS, and triggers the build-assets process which builds the site and deploys the result to public_html. This operation is synchronous: the build and deployment complete before the tool returns, so the website is live as soon as the tool finishes successfully — there is no separate asynchronous build to wait for or poll. For plain PHP applications that should be extracted as-is, use agencyHosting_deployPhpApplication instead. The website UID is automatically resolved from the domain.",
7
+ "method": "",
8
+ "path": "",
9
+ "inputSchema": {
10
+ "type": "object",
11
+ "properties": {
12
+ "domain": {
13
+ "type": "string",
14
+ "description": "Domain name of the Agency Plan website (e.g., example.com)"
15
+ },
16
+ "archivePath": {
17
+ "type": "string",
18
+ "description": "Absolute or relative path to the website archive file. Supported formats: zip, tar, tar.gz, tgz. The archive must contain the application source files. If user provides a directory path, create an archive from it before proceeding using EXACTLY this naming pattern: directoryname_YYYYMMDD_HHMMSS.zip (e.g., mywebsite_20250115_143022.zip)"
19
+ },
20
+ "removeArchive": {
21
+ "type": "boolean",
22
+ "description": "Whether to remove the local archive file after successful deployment (default: true)"
23
+ }
24
+ },
25
+ "required": [
26
+ "domain",
27
+ "archivePath"
28
+ ]
29
+ },
30
+ "security": [],
31
+ "custom": true,
32
+ "templateFile": "deploy-node-static.template.js",
33
+ "templateFileTS": "deploy-node-static.template.ts",
34
+ "handlerMethod": "handleNodeStaticDeploy",
35
+ "group": "agency-hosting"
36
+ },
37
+ {
38
+ "name": "agencyHosting_deployPhpApplication",
39
+ "topic": "agency-hosting",
40
+ "description": "Deploy a PHP (or other non-build) Agency Plan (h5g) website from an archive file. WARNING: this overwrites the website's existing contents and cannot be undone — always confirm with the user before proceeding. Use this for Agency Plan websites where the archive contents should be extracted and served as-is with no build step (e.g., PHP applications). The tool resolves the website from its domain, uploads the archive to the website's file browser over TUS, and triggers the import-archive process which overwrites the website contents with the archive contents. This operation is synchronous: the archive is extracted and deployed before the tool returns, so the website is live as soon as the tool finishes successfully — there is no separate asynchronous build to wait for or poll. For node-static websites that require a build step, use agencyHosting_deployNodeStaticWebsite instead. The website UID is automatically resolved from the domain.",
41
+ "method": "",
42
+ "path": "",
43
+ "inputSchema": {
44
+ "type": "object",
45
+ "properties": {
46
+ "domain": {
47
+ "type": "string",
48
+ "description": "Domain name of the Agency Plan website (e.g., example.com)"
49
+ },
50
+ "archivePath": {
51
+ "type": "string",
52
+ "description": "Absolute or relative path to the website archive file. Supported formats: zip, tar, tar.gz, tgz. If user provides a directory path, create an archive from it before proceeding using EXACTLY this naming pattern: directoryname_YYYYMMDD_HHMMSS.zip (e.g., mywebsite_20250115_143022.zip)"
53
+ },
54
+ "removeArchive": {
55
+ "type": "boolean",
56
+ "description": "Whether to remove the local archive file after successful deployment (default: true)"
57
+ }
58
+ },
59
+ "required": [
60
+ "domain",
61
+ "archivePath"
62
+ ]
63
+ },
64
+ "security": [],
65
+ "custom": true,
66
+ "templateFile": "deploy-php-app.template.js",
67
+ "templateFileTS": "deploy-php-app.template.ts",
68
+ "handlerMethod": "handlePhpAppDeploy",
69
+ "group": "agency-hosting"
70
+ },
3
71
  {
4
72
  "name": "agency-hosting_listAvailableDatacentersForAnAgencyPlanOrderV1",
5
73
  "description": "Lists the datacenters available for provisioning a new website on the given Agency Plan\nhosting order.\n\nEach datacenter includes a `pinger_url` you can ping from the client to measure round-trip\nlatency; comparing the results across datacenters lets you pick the nearest one (lowest\nping) before choosing its `code` as the `datacenter_code` when creating a website setup.",