hostinger-api-mcp 0.1.37 → 0.1.40

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.
@@ -0,0 +1,2106 @@
1
+
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
+ import minimist from 'minimist';
6
+ import cors from "cors";
7
+ import express from "express";
8
+ import axios from "axios";
9
+ import { config as dotenvConfig } from "dotenv";
10
+ import {
11
+ ListToolsRequestSchema,
12
+ CallToolRequestSchema,
13
+ } from "@modelcontextprotocol/sdk/types.js";
14
+ import * as tus from "tus-js-client";
15
+ import fs from "fs";
16
+ import path from "path";
17
+
18
+ // Load environment variables
19
+ dotenvConfig();
20
+
21
+ const SECURITY_SCHEMES = {
22
+ "apiToken": {
23
+ "type": "http",
24
+ "description": "API Token authentication",
25
+ "scheme": "bearer"
26
+ }
27
+ };
28
+
29
+ /**
30
+ * MCP Server for Hostinger API
31
+ * Generated from OpenAPI spec version 0.11.7
32
+ */
33
+ class MCPServer {
34
+ constructor({ name, version, tools }) {
35
+ // Initialize class properties
36
+ this.name = name;
37
+ this.version = version;
38
+ this.toolList = tools;
39
+ this.server = null;
40
+ this.tools = new Map();
41
+ this.debug = process.env.DEBUG === "true";
42
+ this.baseUrl = process.env.API_BASE_URL || "https://developers.hostinger.com";
43
+ this.headers = this.parseHeaders(process.env.API_HEADERS || "");
44
+
45
+ // Initialize tools map - do this before creating server
46
+ this.initializeTools();
47
+
48
+ // Create MCP server with correct capabilities
49
+ this.server = new Server(
50
+ {
51
+ name: this.name,
52
+ version: this.version,
53
+ },
54
+ {
55
+ capabilities: {
56
+ tools: {}, // Enable tools capability
57
+ },
58
+ }
59
+ );
60
+
61
+ // Set up request handlers - don't log here
62
+ this.setupHandlers();
63
+ }
64
+
65
+ /**
66
+ * Parse headers from string
67
+ */
68
+ parseHeaders(headerStr) {
69
+ const headers = {};
70
+ if (headerStr) {
71
+ headerStr.split(",").forEach((header) => {
72
+ const [key, value] = header.split(":");
73
+ if (key && value) headers[key.trim()] = value.trim();
74
+ });
75
+ }
76
+
77
+ headers['User-Agent'] = `hostinger-mcp-server/${this.version}`;
78
+
79
+ return headers;
80
+ }
81
+
82
+ /**
83
+ * Initialize tools map from OpenAPI spec
84
+ * This runs before the server is connected, so don't log here
85
+ */
86
+ initializeTools() {
87
+ // Initialize each tool in the tools map
88
+ for (const tool of this.toolList) {
89
+ this.tools.set(tool.name, {
90
+ name: tool.name,
91
+ description: tool.description,
92
+ inputSchema: tool.inputSchema,
93
+ // Don't include security at the tool level
94
+ });
95
+ }
96
+
97
+ // Don't log here, we're not connected yet
98
+ console.error(`Initialized ${this.tools.size} tools`);
99
+ }
100
+
101
+ /**
102
+ * Set up request handlers
103
+ */
104
+ setupHandlers() {
105
+ // Handle tool listing requests
106
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
107
+ this.log('debug', "Handling ListTools request");
108
+ // Return tools in the format expected by MCP SDK
109
+ return {
110
+ tools: Array.from(this.tools.entries()).map(([id, tool]) => ({
111
+ id,
112
+ ...tool,
113
+ })),
114
+ };
115
+ });
116
+
117
+ // Handle tool execution requests
118
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
119
+ const { id, name, arguments: params } = request.params;
120
+ this.log('debug', "Handling CallTool request", { id, name, params });
121
+
122
+ let toolName;
123
+ let toolDetails;
124
+
125
+ // Find the requested tool
126
+ for (const [tid, tool] of this.tools.entries()) {
127
+ if (tool.name === name) {
128
+ toolName = name;
129
+ break;
130
+ }
131
+ }
132
+
133
+ if (!toolName) {
134
+ throw new Error(`Tool not found: ${name}`);
135
+ }
136
+
137
+ toolDetails = this.toolList.find(t => t.name === toolName);
138
+ if (!toolDetails) {
139
+ throw new Error(`Tool details not found for ID: ${toolName}`);
140
+ }
141
+
142
+ try {
143
+ this.log('info', `Executing tool: ${toolName}`);
144
+
145
+ let result;
146
+
147
+ if (toolDetails.custom) {
148
+ result = await this.executeCustomTool(toolDetails, params || {});
149
+ } else {
150
+ result = await this.executeApiCall(toolDetails, params || {});
151
+ }
152
+
153
+ // Return the result in the correct MCP format
154
+ return {
155
+ content: [
156
+ {
157
+ type: "text",
158
+ text: JSON.stringify(result)
159
+ }
160
+ ]
161
+ };
162
+
163
+ } catch (error) {
164
+ const errorMessage = error instanceof Error ? error.message : String(error);
165
+ const response = error.response;
166
+ this.log('error', `Error executing tool ${name}: ${errorMessage}`);
167
+
168
+ throw error;
169
+ }
170
+ });
171
+ }
172
+
173
+ async executeCustomTool(tool, params) {
174
+ switch (tool.name) {
175
+ case 'hosting_importWordpressWebsite':
176
+ return await this.handleWordpressWebsiteImport(params);
177
+ case 'hosting_deployWordpressPlugin':
178
+ return await this.handleWordpressPluginDeploy(params);
179
+ case 'hosting_deployWordpressTheme':
180
+ return await this.handleWordpressThemeDeploy(params);
181
+ case 'hosting_deployJsApplication':
182
+ return await this.handleJavascriptApplicationDeploy(params);
183
+ case 'hosting_deployStaticWebsite':
184
+ return await this.handleStaticWebsiteDeploy(params);
185
+ case 'hosting_listJsDeployments':
186
+ return await this.handleListJavascriptDeployments(params);
187
+ case 'hosting_showJsDeploymentLogs':
188
+ return await this.handleShowJsDeploymentLogs(params);
189
+ default:
190
+ throw new Error(`Unknown custom tool: ${tool.name}`);
191
+ }
192
+ }
193
+
194
+ normalizePath(pathString) {
195
+ return pathString.replace(/\\/g, '/');
196
+ }
197
+
198
+ async resolveUsername(domain) {
199
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
200
+ const url = new URL(`api/hosting/v1/websites?domain=${encodeURIComponent(domain)}`, baseUrl).toString();
201
+
202
+ try {
203
+ const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
204
+ if (!bearerToken) {
205
+ throw new Error('API_TOKEN environment variable not found');
206
+ }
207
+
208
+ const config = {
209
+ method: 'get',
210
+ url,
211
+ headers: {
212
+ ...this.headers,
213
+ 'Authorization': `Bearer ${bearerToken}`
214
+ },
215
+ timeout: 60000, // 60s
216
+ validateStatus: function (status) {
217
+ return status < 500;
218
+ }
219
+ };
220
+
221
+ const response = await axios(config);
222
+
223
+ if (response.status !== 200) {
224
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
225
+ }
226
+
227
+ const websites = response.data?.data;
228
+ if (!websites || websites.length === 0) {
229
+ throw new Error(`No website found for domain: ${domain}`);
230
+ }
231
+
232
+ const username = websites[0].username;
233
+ if (!username) {
234
+ throw new Error(`Username not found in website data for domain: ${domain}`);
235
+ }
236
+
237
+ this.log('info', `Resolved username: ${username} for domain: ${domain}`);
238
+ return username;
239
+
240
+ } catch (error) {
241
+ const errorMessage = error instanceof Error ? error.message : String(error);
242
+ this.log('error', `Failed to resolve username for domain ${domain}: ${errorMessage}`);
243
+
244
+ if (axios.isAxiosError(error)) {
245
+ const responseData = error.response?.data;
246
+ const responseStatus = error.response?.status;
247
+ this.log('error', 'API Error Details:', {
248
+ status: responseStatus,
249
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
250
+ });
251
+ }
252
+
253
+ throw error;
254
+ }
255
+ }
256
+
257
+ async fetchUploadCredentials(username, domain) {
258
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
259
+ const url = new URL('api/hosting/v1/files/upload-urls', baseUrl).toString();
260
+
261
+ try {
262
+ const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
263
+ if (!bearerToken) {
264
+ throw new Error('API_TOKEN environment variable not found');
265
+ }
266
+
267
+ const config = {
268
+ method: 'post',
269
+ url,
270
+ headers: {
271
+ ...this.headers,
272
+ 'Authorization': `Bearer ${bearerToken}`,
273
+ 'Content-Type': 'application/json'
274
+ },
275
+ data: {
276
+ username,
277
+ domain
278
+ },
279
+ timeout: 60000, // 60s
280
+ validateStatus: function (status) {
281
+ return status < 500;
282
+ }
283
+ };
284
+
285
+ const response = await axios(config);
286
+
287
+ if (response.status !== 200) {
288
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
289
+ }
290
+
291
+ return response.data;
292
+
293
+ } catch (error) {
294
+ const errorMessage = error instanceof Error ? error.message : String(error);
295
+ this.log('error', `Failed to fetch upload credentials: ${errorMessage}`);
296
+
297
+ if (axios.isAxiosError(error)) {
298
+ const responseData = error.response?.data;
299
+ const responseStatus = error.response?.status;
300
+ this.log('error', 'API Error Details:', {
301
+ status: responseStatus,
302
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
303
+ });
304
+ }
305
+
306
+ throw error;
307
+ }
308
+ }
309
+
310
+ async uploadFile(filePath, relativePath, uploadUrl, authRestToken, authToken) {
311
+ return new Promise(async (resolve, reject) => {
312
+ try {
313
+ const stats = fs.statSync(filePath);
314
+ const fileStream = fs.createReadStream(filePath);
315
+
316
+ const cleanUploadUrl = uploadUrl.replace(new RegExp('/$'), '');
317
+ const normalizedPath = this.normalizePath(relativePath);
318
+ const uploadUrlWithFile = `${cleanUploadUrl}/${normalizedPath}?override=true`;
319
+
320
+ const requestHeaders = {
321
+ 'X-Auth': authToken,
322
+ 'X-Auth-Rest': authRestToken,
323
+ 'upload-length': stats.size.toString(),
324
+ 'upload-offset': '0'
325
+ };
326
+
327
+ try {
328
+ this.log('debug', `Making pre-upload POST request to ${uploadUrlWithFile}`);
329
+ await axios.post(uploadUrlWithFile, '', {
330
+ headers: requestHeaders,
331
+ timeout: 60000, // 60s
332
+ validateStatus: function (status) {
333
+ return status == 201;
334
+ }
335
+ });
336
+ } catch (error) {
337
+ const errorMessage = error instanceof Error ? error.message : String(error);
338
+
339
+ if (axios.isAxiosError(error)) {
340
+ const responseData = error.response?.data;
341
+ const responseStatus = error.response?.status;
342
+ const responseHeaders = error.response?.headers;
343
+ const responseText = typeof responseData === 'object' ? JSON.stringify(responseData) : responseData;
344
+
345
+ this.log('error', 'Pre-upload POST request failed - Full Response Details:', {
346
+ status: responseStatus,
347
+ headers: responseHeaders,
348
+ data: responseText,
349
+ message: errorMessage
350
+ });
351
+ reject(new Error(`Pre-upload request failed: ${errorMessage}`));
352
+ return;
353
+ } else {
354
+ this.log('error', `Pre-upload POST request failed: ${errorMessage}`);
355
+ reject(new Error(`Pre-upload request failed: ${errorMessage}`));
356
+ return;
357
+ }
358
+ }
359
+
360
+ const upload = new tus.Upload(fileStream, {
361
+ uploadUrl: uploadUrlWithFile,
362
+ retryDelays: [1000, 2000, 4000, 8000, 16000, 20000],
363
+ uploadDataDuringCreation: false,
364
+ parallelUploads: 1,
365
+ chunkSize: 10485760,
366
+ headers: requestHeaders,
367
+ removeFingerprintOnSuccess: true,
368
+ uploadSize: stats.size,
369
+ metadata: {
370
+ filename: path.basename(relativePath)
371
+ },
372
+ onError: (error) => {
373
+ this.log('error', `TUS upload error for ${relativePath}`, { error: error.message });
374
+ reject(new Error(`Upload failed: ${error.message}`));
375
+ },
376
+ onSuccess: () => {
377
+ this.log('info', `TUS upload completed for ${relativePath}`, { url: upload.url });
378
+ resolve({
379
+ url: upload.url,
380
+ filename: relativePath
381
+ });
382
+ }
383
+ });
384
+
385
+ upload.start();
386
+
387
+ } catch (error) {
388
+ const errorMessage = error instanceof Error ? error.message : String(error);
389
+ this.log('error', `Error preparing upload for ${filePath}`, { error: errorMessage });
390
+ reject(new Error(`Failed to prepare upload: ${errorMessage}`));
391
+ }
392
+ });
393
+ }
394
+
395
+ hosting_importWordpressWebsite_validateArchiveFormat(filePath) {
396
+ const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
397
+ const fileName = path.basename(filePath).toLowerCase();
398
+
399
+ for (const ext of validExtensions) {
400
+ if (fileName.endsWith(`.${ext}`)) {
401
+ return true;
402
+ }
403
+ }
404
+
405
+ return false;
406
+ }
407
+
408
+ hosting_importWordpressWebsite_validateRequiredParams(params) {
409
+ const { domain, archivePath, databaseDump } = params;
410
+
411
+ if (!domain || typeof domain !== 'string') {
412
+ throw new Error('domain is required and must be a string');
413
+ }
414
+
415
+ if (!archivePath || typeof archivePath !== 'string') {
416
+ throw new Error('archivePath is required and must be a string');
417
+ }
418
+
419
+ if (!databaseDump || typeof databaseDump !== 'string') {
420
+ throw new Error('databaseDump is required and must be a string');
421
+ }
422
+ }
423
+
424
+ hosting_importWordpressWebsite_validateArchiveFile(archivePath) {
425
+ if (!fs.existsSync(archivePath)) {
426
+ throw new Error(`Archive file not found: ${archivePath}`);
427
+ }
428
+
429
+ const archiveStats = fs.statSync(archivePath);
430
+ if (!archiveStats.isFile()) {
431
+ throw new Error(`Archive path is not a file: ${archivePath}`);
432
+ }
433
+
434
+ if (!this.hosting_importWordpressWebsite_validateArchiveFormat(archivePath)) {
435
+ throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
436
+ }
437
+ }
438
+
439
+ hosting_importWordpressWebsite_validateDatabaseFile(databaseDump) {
440
+ if (!fs.existsSync(databaseDump)) {
441
+ throw new Error(`Database dump file not found: ${databaseDump}`);
442
+ }
443
+
444
+ const dbStats = fs.statSync(databaseDump);
445
+ if (!dbStats.isFile()) {
446
+ throw new Error(`Database dump path is not a file: ${databaseDump}`);
447
+ }
448
+
449
+ if (!databaseDump.toLowerCase().endsWith('.sql')) {
450
+ throw new Error('Database dump must be a .sql file');
451
+ }
452
+ }
453
+
454
+ async hosting_importWordpressWebsite_checkWebsiteIsEmpty(username, domain) {
455
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
456
+ const url = new URL(`api/hosting/v1/accounts/${username}/domains/${domain}/is-empty`, baseUrl).toString();
457
+
458
+ try {
459
+ const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
460
+ if (!bearerToken) {
461
+ throw new Error('API_TOKEN environment variable not found');
462
+ }
463
+
464
+ const config = {
465
+ method: 'get',
466
+ url,
467
+ headers: {
468
+ ...this.headers,
469
+ 'Authorization': `Bearer ${bearerToken}`
470
+ },
471
+ timeout: 60000, // 60s
472
+ validateStatus: function (status) {
473
+ return status < 500;
474
+ }
475
+ };
476
+
477
+ const response = await axios(config);
478
+
479
+ if (response.status !== 200) {
480
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
481
+ }
482
+
483
+ const { is_empty } = response.data;
484
+
485
+ if (!is_empty) {
486
+ 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.');
487
+ }
488
+
489
+ this.log('info', `Website ${domain} is empty, proceeding with import`);
490
+ return true;
491
+
492
+ } catch (error) {
493
+ const errorMessage = error instanceof Error ? error.message : String(error);
494
+ this.log('error', `Failed to check if website is empty: ${errorMessage}`);
495
+
496
+ if (axios.isAxiosError(error)) {
497
+ const responseData = error.response?.data;
498
+ const responseStatus = error.response?.status;
499
+ this.log('error', 'API Error Details:', {
500
+ status: responseStatus,
501
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
502
+ });
503
+ }
504
+
505
+ throw error;
506
+ }
507
+ }
508
+
509
+ async hosting_importWordpressWebsite_extractFiles(username, domain, archivePath, databaseDump) {
510
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
511
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/import`, baseUrl).toString();
512
+
513
+ try {
514
+ const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
515
+ if (!bearerToken) {
516
+ throw new Error('API_TOKEN environment variable not found');
517
+ }
518
+
519
+ const config = {
520
+ method: 'post',
521
+ url,
522
+ headers: {
523
+ ...this.headers,
524
+ 'Authorization': `Bearer ${bearerToken}`,
525
+ 'Content-Type': 'application/json'
526
+ },
527
+ data: {
528
+ archive_path: path.basename(archivePath),
529
+ sql_path: path.basename(databaseDump)
530
+ },
531
+ timeout: 60000, // 60s
532
+ validateStatus: function (status) {
533
+ return status < 500;
534
+ }
535
+ };
536
+
537
+ const response = await axios(config);
538
+
539
+ this.log('info', `Successfully triggered file extraction for ${domain}`);
540
+ return true;
541
+
542
+ } catch (error) {
543
+ const errorMessage = error instanceof Error ? error.message : String(error);
544
+ this.log('error', `Failed to trigger file extraction: ${errorMessage}`);
545
+
546
+ if (axios.isAxiosError(error)) {
547
+ const responseData = error.response?.data;
548
+ const responseStatus = error.response?.status;
549
+ this.log('error', 'API Error Details:', {
550
+ status: responseStatus,
551
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
552
+ });
553
+ }
554
+
555
+ throw error;
556
+ }
557
+ }
558
+
559
+ async handleWordpressWebsiteImport(params) {
560
+ const { domain, archivePath, databaseDump } = params;
561
+
562
+ this.hosting_importWordpressWebsite_validateRequiredParams(params);
563
+ this.hosting_importWordpressWebsite_validateArchiveFile(archivePath);
564
+ this.hosting_importWordpressWebsite_validateDatabaseFile(databaseDump);
565
+
566
+ // Auto-resolve username from domain
567
+ this.log('info', `Resolving username from domain: ${domain}`);
568
+ const username = await this.resolveUsername(domain);
569
+
570
+ await this.hosting_importWordpressWebsite_checkWebsiteIsEmpty(username, domain);
571
+
572
+ const filesToUpload = [{
573
+ absolutePath: archivePath,
574
+ relativePath: path.basename(archivePath),
575
+ type: 'archive'
576
+ }, {
577
+ absolutePath: databaseDump,
578
+ relativePath: path.basename(databaseDump),
579
+ type: 'database'
580
+ }];
581
+
582
+ let uploadCredentials;
583
+ try {
584
+ uploadCredentials = await this.fetchUploadCredentials(username, domain);
585
+ } catch (error) {
586
+ const errorMessage = error instanceof Error ? error.message : String(error);
587
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
588
+ }
589
+
590
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
591
+
592
+ if (!uploadUrl || !authToken || !authRestToken) {
593
+ throw new Error('Invalid upload credentials received from API');
594
+ }
595
+
596
+ this.log('info', `Starting website archive import to ${uploadUrl}`);
597
+
598
+ const results = [];
599
+ let successCount = 0;
600
+ let failureCount = 0;
601
+
602
+ for (const fileInfo of filesToUpload) {
603
+ try {
604
+ this.log('info', `Uploading ${fileInfo.type}: ${fileInfo.absolutePath}`);
605
+
606
+ const stats = fs.statSync(fileInfo.absolutePath);
607
+ const uploadResult = await this.uploadFile(
608
+ fileInfo.absolutePath,
609
+ fileInfo.relativePath,
610
+ uploadUrl,
611
+ authRestToken,
612
+ authToken
613
+ );
614
+
615
+ results.push({
616
+ file: fileInfo.absolutePath,
617
+ remotePath: fileInfo.relativePath,
618
+ type: fileInfo.type,
619
+ status: 'success',
620
+ uploadUrl: uploadResult.url,
621
+ size: stats.size
622
+ });
623
+
624
+ successCount++;
625
+ this.log('info', `Successfully uploaded ${fileInfo.type}: ${fileInfo.relativePath}`);
626
+
627
+ } catch (error) {
628
+ const errorMessage = error instanceof Error ? error.message : String(error);
629
+ results.push({
630
+ file: fileInfo.absolutePath,
631
+ remotePath: fileInfo.relativePath,
632
+ type: fileInfo.type,
633
+ status: 'error',
634
+ error: errorMessage
635
+ });
636
+
637
+ failureCount++;
638
+ this.log('error', `Failed to upload ${fileInfo.type} ${fileInfo.absolutePath}: ${errorMessage}`);
639
+ }
640
+ }
641
+
642
+ const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
643
+
644
+ if (failureCount === 0) {
645
+ try {
646
+ this.log('info', 'All files uploaded successfully, triggering extraction...');
647
+ await this.hosting_importWordpressWebsite_extractFiles(username, domain, archivePath, databaseDump);
648
+ } catch (error) {
649
+ const errorMessage = error instanceof Error ? error.message : String(error);
650
+ this.log('error', `File extraction failed: ${errorMessage}`);
651
+ return {
652
+ status: 'partial',
653
+ summary: {
654
+ total: filesToUpload.length,
655
+ successful: successCount,
656
+ failed: failureCount
657
+ },
658
+ results,
659
+ extractionError: errorMessage
660
+ };
661
+ }
662
+ }
663
+
664
+ return {
665
+ status: overallStatus,
666
+ summary: {
667
+ total: filesToUpload.length,
668
+ successful: successCount,
669
+ failed: failureCount
670
+ },
671
+ results
672
+ };
673
+ }
674
+
675
+ hosting_deployWordpressPlugin_generateRandomString(length) {
676
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
677
+ let result = '';
678
+ for (let i = 0; i < length; i++) {
679
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
680
+ }
681
+ return result;
682
+ }
683
+
684
+ hosting_deployWordpressPlugin_validateRequiredParams(params) {
685
+ const { domain, slug, pluginPath } = params;
686
+
687
+ if (!domain || typeof domain !== 'string') {
688
+ throw new Error('domain is required and must be a string');
689
+ }
690
+
691
+ if (!slug || typeof slug !== 'string') {
692
+ throw new Error('slug is required and must be a string');
693
+ }
694
+
695
+ if (!pluginPath || typeof pluginPath !== 'string') {
696
+ throw new Error('pluginPath is required and must be a string');
697
+ }
698
+ }
699
+
700
+ hosting_deployWordpressPlugin_validatePluginDirectory(pluginPath) {
701
+ if (!fs.existsSync(pluginPath)) {
702
+ throw new Error(`Plugin directory not found: ${pluginPath}`);
703
+ }
704
+
705
+ const pluginStats = fs.statSync(pluginPath);
706
+ if (!pluginStats.isDirectory()) {
707
+ throw new Error(`Plugin path is not a directory: ${pluginPath}`);
708
+ }
709
+ }
710
+
711
+ hosting_deployWordpressPlugin_scanDirectory(dirPath, basePath = dirPath) {
712
+ const files = [];
713
+
714
+ const scanDir = (currentPath) => {
715
+ const items = fs.readdirSync(currentPath);
716
+
717
+ for (const item of items) {
718
+ const itemPath = path.join(currentPath, item);
719
+ const stats = fs.statSync(itemPath);
720
+
721
+ if (stats.isDirectory()) {
722
+ scanDir(itemPath);
723
+ } else if (stats.isFile()) {
724
+ const relativePath = path.relative(basePath, itemPath);
725
+ files.push({
726
+ absolutePath: itemPath,
727
+ relativePath: relativePath
728
+ });
729
+ }
730
+ }
731
+ };
732
+
733
+ scanDir(dirPath);
734
+ return files;
735
+ }
736
+
737
+ async hosting_deployWordpressPlugin_deployPlugin(username, domain, slug, pluginPath) {
738
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
739
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/plugins/deploy`, baseUrl).toString();
740
+
741
+ try {
742
+ const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
743
+ if (!bearerToken) {
744
+ throw new Error('API_TOKEN environment variable not found');
745
+ }
746
+
747
+ const config = {
748
+ method: 'post',
749
+ url,
750
+ headers: {
751
+ ...this.headers,
752
+ 'Authorization': `Bearer ${bearerToken}`,
753
+ 'Content-Type': 'application/json'
754
+ },
755
+ data: {
756
+ slug,
757
+ plugin_path: pluginPath
758
+ },
759
+ timeout: 60000, // 60s
760
+ validateStatus: function (status) {
761
+ return status < 500;
762
+ }
763
+ };
764
+
765
+ const response = await axios(config);
766
+
767
+ if (response.status !== 200) {
768
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
769
+ }
770
+
771
+ this.log('info', `Successfully triggered plugin deployment for ${domain}`);
772
+ return true;
773
+
774
+ } catch (error) {
775
+ const errorMessage = error instanceof Error ? error.message : String(error);
776
+ this.log('error', `Failed to trigger plugin deployment: ${errorMessage}`);
777
+
778
+ if (axios.isAxiosError(error)) {
779
+ const responseData = error.response?.data;
780
+ const responseStatus = error.response?.status;
781
+ this.log('error', 'API Error Details:', {
782
+ status: responseStatus,
783
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
784
+ });
785
+ }
786
+
787
+ throw error;
788
+ }
789
+ }
790
+
791
+ async handleWordpressPluginDeploy(params) {
792
+ const { domain, slug, pluginPath } = params;
793
+
794
+ this.hosting_deployWordpressPlugin_validateRequiredParams(params);
795
+ this.hosting_deployWordpressPlugin_validatePluginDirectory(pluginPath);
796
+
797
+ this.log('info', `Resolving username from domain: ${domain}`);
798
+ const username = await this.resolveUsername(domain);
799
+
800
+ const randomSuffix = this.hosting_deployWordpressPlugin_generateRandomString(8);
801
+ const uploadDirName = `${slug}-${randomSuffix}`;
802
+
803
+ this.log('info', `Scanning plugin directory: ${pluginPath}`);
804
+ const pluginFiles = this.hosting_deployWordpressPlugin_scanDirectory(pluginPath);
805
+
806
+ if (pluginFiles.length === 0) {
807
+ throw new Error(`No files found in plugin directory: ${pluginPath}`);
808
+ }
809
+
810
+ this.log('info', `Found ${pluginFiles.length} files to upload`);
811
+
812
+ let uploadCredentials;
813
+ try {
814
+ uploadCredentials = await this.fetchUploadCredentials(username, domain);
815
+ } catch (error) {
816
+ const errorMessage = error instanceof Error ? error.message : String(error);
817
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
818
+ }
819
+
820
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
821
+
822
+ if (!uploadUrl || !authToken || !authRestToken) {
823
+ throw new Error('Invalid upload credentials received from API');
824
+ }
825
+
826
+ this.log('info', `Starting plugin file upload to ${uploadUrl}`);
827
+
828
+ const results = [];
829
+ let successCount = 0;
830
+ let failureCount = 0;
831
+
832
+ for (const fileInfo of pluginFiles) {
833
+ try {
834
+ const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
835
+ const uploadPath = `wp-content/plugins/${uploadDirName}/${normalizedRelativePath}`;
836
+ this.log('info', `Uploading: ${fileInfo.absolutePath} -> ${uploadPath}`);
837
+
838
+ const stats = fs.statSync(fileInfo.absolutePath);
839
+ const uploadResult = await this.uploadFile(
840
+ fileInfo.absolutePath,
841
+ uploadPath,
842
+ uploadUrl,
843
+ authRestToken,
844
+ authToken
845
+ );
846
+
847
+ results.push({
848
+ file: fileInfo.absolutePath,
849
+ remotePath: uploadPath,
850
+ status: 'success',
851
+ uploadUrl: uploadResult.url,
852
+ size: stats.size
853
+ });
854
+
855
+ successCount++;
856
+ this.log('info', `Successfully uploaded: ${uploadPath}`);
857
+
858
+ } catch (error) {
859
+ const errorMessage = error instanceof Error ? error.message : String(error);
860
+ const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
861
+ const uploadPath = `wp-content/plugins/${uploadDirName}/${normalizedRelativePath}`;
862
+
863
+ results.push({
864
+ file: fileInfo.absolutePath,
865
+ remotePath: uploadPath,
866
+ status: 'error',
867
+ error: errorMessage
868
+ });
869
+
870
+ failureCount++;
871
+ this.log('error', `Failed to upload ${fileInfo.absolutePath}: ${errorMessage}`);
872
+ }
873
+ }
874
+
875
+ const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
876
+
877
+ if (failureCount === 0) {
878
+ try {
879
+ this.log('info', 'All files uploaded successfully, triggering plugin deployment...');
880
+ await this.hosting_deployWordpressPlugin_deployPlugin(username, domain, slug, uploadDirName);
881
+ } catch (error) {
882
+ const errorMessage = error instanceof Error ? error.message : String(error);
883
+ this.log('error', `Plugin deployment failed: ${errorMessage}`);
884
+ return {
885
+ status: 'partial',
886
+ summary: {
887
+ total: pluginFiles.length,
888
+ successful: successCount,
889
+ failed: failureCount
890
+ },
891
+ results,
892
+ deploymentError: errorMessage
893
+ };
894
+ }
895
+ }
896
+
897
+ return {
898
+ status: overallStatus,
899
+ summary: {
900
+ total: pluginFiles.length,
901
+ successful: successCount,
902
+ failed: failureCount
903
+ },
904
+ results,
905
+ uploadDirName
906
+ };
907
+ }
908
+
909
+ hosting_deployWordpressTheme_generateRandomString(length) {
910
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
911
+ let result = '';
912
+ for (let i = 0; i < length; i++) {
913
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
914
+ }
915
+ return result;
916
+ }
917
+
918
+ hosting_deployWordpressTheme_validateRequiredParams(params) {
919
+ const { domain, slug, themePath } = params;
920
+
921
+ if (!domain || typeof domain !== 'string') {
922
+ throw new Error('domain is required and must be a string');
923
+ }
924
+
925
+ if (!slug || typeof slug !== 'string') {
926
+ throw new Error('slug is required and must be a string');
927
+ }
928
+
929
+ if (!themePath || typeof themePath !== 'string') {
930
+ throw new Error('themePath is required and must be a string');
931
+ }
932
+ }
933
+
934
+ hosting_deployWordpressTheme_validateThemeDirectory(themePath) {
935
+ if (!fs.existsSync(themePath)) {
936
+ throw new Error(`Theme directory not found: ${themePath}`);
937
+ }
938
+
939
+ const themeStats = fs.statSync(themePath);
940
+ if (!themeStats.isDirectory()) {
941
+ throw new Error(`Theme path is not a directory: ${themePath}`);
942
+ }
943
+ }
944
+
945
+ hosting_deployWordpressTheme_scanDirectory(dirPath, basePath = dirPath) {
946
+ const files = [];
947
+
948
+ const scanDir = (currentPath) => {
949
+ const items = fs.readdirSync(currentPath);
950
+
951
+ for (const item of items) {
952
+ const itemPath = path.join(currentPath, item);
953
+ const stats = fs.statSync(itemPath);
954
+
955
+ if (stats.isDirectory()) {
956
+ scanDir(itemPath);
957
+ } else if (stats.isFile()) {
958
+ const relativePath = path.relative(basePath, itemPath);
959
+ files.push({
960
+ absolutePath: itemPath,
961
+ relativePath: relativePath
962
+ });
963
+ }
964
+ }
965
+ };
966
+
967
+ scanDir(dirPath);
968
+ return files;
969
+ }
970
+
971
+ async hosting_deployWordpressTheme_deployTheme(username, domain, slug, themePath, activate = false) {
972
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
973
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/themes/deploy`, baseUrl).toString();
974
+
975
+ try {
976
+ const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
977
+ if (!bearerToken) {
978
+ throw new Error('API_TOKEN environment variable not found');
979
+ }
980
+
981
+ const config = {
982
+ method: 'post',
983
+ url,
984
+ headers: {
985
+ ...this.headers,
986
+ 'Authorization': `Bearer ${bearerToken}`,
987
+ 'Content-Type': 'application/json'
988
+ },
989
+ data: {
990
+ slug,
991
+ theme_path: themePath,
992
+ is_activated: activate
993
+ },
994
+ timeout: 60000, // 60s
995
+ validateStatus: function (status) {
996
+ return status < 500;
997
+ }
998
+ };
999
+
1000
+ const response = await axios(config);
1001
+
1002
+ if (response.status !== 200) {
1003
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1004
+ }
1005
+
1006
+ this.log('info', `Successfully triggered theme deployment for ${domain}`);
1007
+ return true;
1008
+
1009
+ } catch (error) {
1010
+ const errorMessage = error instanceof Error ? error.message : String(error);
1011
+ this.log('error', `Failed to trigger theme deployment: ${errorMessage}`);
1012
+
1013
+ if (axios.isAxiosError(error)) {
1014
+ const responseData = error.response?.data;
1015
+ const responseStatus = error.response?.status;
1016
+ this.log('error', 'API Error Details:', {
1017
+ status: responseStatus,
1018
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1019
+ });
1020
+ }
1021
+
1022
+ throw error;
1023
+ }
1024
+ }
1025
+
1026
+ async handleWordpressThemeDeploy(params) {
1027
+ const { domain, slug, themePath, activate = false } = params;
1028
+
1029
+ this.hosting_deployWordpressTheme_validateRequiredParams(params);
1030
+ this.hosting_deployWordpressTheme_validateThemeDirectory(themePath);
1031
+
1032
+ this.log('info', `Resolving username from domain: ${domain}`);
1033
+ const username = await this.resolveUsername(domain);
1034
+
1035
+ const randomSuffix = this.hosting_deployWordpressTheme_generateRandomString(8);
1036
+ const uploadDirName = `${slug}-${randomSuffix}`;
1037
+
1038
+ this.log('info', `Scanning theme directory: ${themePath}`);
1039
+ const themeFiles = this.hosting_deployWordpressTheme_scanDirectory(themePath);
1040
+
1041
+ if (themeFiles.length === 0) {
1042
+ throw new Error(`No files found in theme directory: ${themePath}`);
1043
+ }
1044
+
1045
+ this.log('info', `Found ${themeFiles.length} files to upload`);
1046
+
1047
+ let uploadCredentials;
1048
+ try {
1049
+ uploadCredentials = await this.fetchUploadCredentials(username, domain);
1050
+ } catch (error) {
1051
+ const errorMessage = error instanceof Error ? error.message : String(error);
1052
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
1053
+ }
1054
+
1055
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
1056
+
1057
+ if (!uploadUrl || !authToken || !authRestToken) {
1058
+ throw new Error('Invalid upload credentials received from API');
1059
+ }
1060
+
1061
+ this.log('info', `Starting theme file upload to ${uploadUrl}`);
1062
+
1063
+ const results = [];
1064
+ let successCount = 0;
1065
+ let failureCount = 0;
1066
+
1067
+ for (const fileInfo of themeFiles) {
1068
+ try {
1069
+ const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
1070
+ const uploadPath = `wp-content/themes/${uploadDirName}/${normalizedRelativePath}`;
1071
+ this.log('info', `Uploading: ${fileInfo.absolutePath} -> ${uploadPath}`);
1072
+
1073
+ const stats = fs.statSync(fileInfo.absolutePath);
1074
+ const uploadResult = await this.uploadFile(
1075
+ fileInfo.absolutePath,
1076
+ uploadPath,
1077
+ uploadUrl,
1078
+ authRestToken,
1079
+ authToken
1080
+ );
1081
+
1082
+ results.push({
1083
+ file: fileInfo.absolutePath,
1084
+ remotePath: uploadPath,
1085
+ status: 'success',
1086
+ uploadUrl: uploadResult.url,
1087
+ size: stats.size
1088
+ });
1089
+
1090
+ successCount++;
1091
+ this.log('info', `Successfully uploaded: ${uploadPath}`);
1092
+
1093
+ } catch (error) {
1094
+ const errorMessage = error instanceof Error ? error.message : String(error);
1095
+ const normalizedRelativePath = this.normalizePath(fileInfo.relativePath);
1096
+ const uploadPath = `wp-content/themes/${uploadDirName}/${normalizedRelativePath}`;
1097
+
1098
+ results.push({
1099
+ file: fileInfo.absolutePath,
1100
+ remotePath: uploadPath,
1101
+ status: 'error',
1102
+ error: errorMessage
1103
+ });
1104
+
1105
+ failureCount++;
1106
+ this.log('error', `Failed to upload ${fileInfo.absolutePath}: ${errorMessage}`);
1107
+ }
1108
+ }
1109
+
1110
+ const overallStatus = failureCount === 0 ? 'success' : (successCount === 0 ? 'failure' : 'partial');
1111
+
1112
+ if (failureCount === 0) {
1113
+ try {
1114
+ this.log('info', 'All files uploaded successfully, triggering theme deployment...');
1115
+ await this.hosting_deployWordpressTheme_deployTheme(username, domain, slug, uploadDirName, activate);
1116
+ } catch (error) {
1117
+ const errorMessage = error instanceof Error ? error.message : String(error);
1118
+ this.log('error', `Theme deployment failed: ${errorMessage}`);
1119
+ return {
1120
+ status: 'partial',
1121
+ summary: {
1122
+ total: themeFiles.length,
1123
+ successful: successCount,
1124
+ failed: failureCount
1125
+ },
1126
+ results,
1127
+ deploymentError: errorMessage
1128
+ };
1129
+ }
1130
+ }
1131
+
1132
+ return {
1133
+ status: overallStatus,
1134
+ summary: {
1135
+ total: themeFiles.length,
1136
+ successful: successCount,
1137
+ failed: failureCount
1138
+ },
1139
+ results,
1140
+ uploadDirName,
1141
+ activated: activate
1142
+ };
1143
+ }
1144
+
1145
+ hosting_deployJsApplication_validateArchiveFormat(filePath) {
1146
+ const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
1147
+ const fileName = path.basename(filePath).toLowerCase();
1148
+
1149
+ for (const ext of validExtensions) {
1150
+ if (fileName.endsWith(`.${ext}`)) {
1151
+ return true;
1152
+ }
1153
+ }
1154
+
1155
+ return false;
1156
+ }
1157
+
1158
+ hosting_deployJsApplication_validateRequiredParams(params) {
1159
+ const { domain, archivePath, removeArchive } = params;
1160
+
1161
+ if (!domain || typeof domain !== 'string') {
1162
+ throw new Error('domain is required and must be a string');
1163
+ }
1164
+
1165
+ if (!archivePath || typeof archivePath !== 'string') {
1166
+ throw new Error('archivePath is required and must be a string');
1167
+ }
1168
+
1169
+ if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
1170
+ throw new Error('removeArchive must be a boolean if provided');
1171
+ }
1172
+ }
1173
+
1174
+ hosting_deployJsApplication_removeArchive(archivePath, removeArchive) {
1175
+ if (!removeArchive) {
1176
+ return false;
1177
+ }
1178
+
1179
+ try {
1180
+ this.log('info', `Removing archive file: ${archivePath}`);
1181
+ fs.unlinkSync(archivePath);
1182
+ this.log('info', `Successfully removed archive file: ${archivePath}`);
1183
+ return true;
1184
+ } catch (error) {
1185
+ const errorMessage = error instanceof Error ? error.message : String(error);
1186
+ this.log('error', `Failed to remove archive file: ${errorMessage}`);
1187
+ // Don't fail the entire operation if archive removal fails
1188
+ return false;
1189
+ }
1190
+ }
1191
+
1192
+ hosting_deployJsApplication_validateArchiveFile(archivePath) {
1193
+ if (!fs.existsSync(archivePath)) {
1194
+ throw new Error(`Archive file not found: ${archivePath}`);
1195
+ }
1196
+
1197
+ const archiveStats = fs.statSync(archivePath);
1198
+ if (!archiveStats.isFile()) {
1199
+ throw new Error(`Archive path is not a file: ${archivePath}`);
1200
+ }
1201
+
1202
+ if (!this.hosting_deployJsApplication_validateArchiveFormat(archivePath)) {
1203
+ throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
1204
+ }
1205
+ }
1206
+
1207
+ async hosting_deployJsApplication_fetchBuildSettings(username, domain, archivePath) {
1208
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1209
+ const archiveBasename = path.basename(archivePath);
1210
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/settings/from-archive?archive_path=${encodeURIComponent(archiveBasename)}`, baseUrl).toString();
1211
+
1212
+ try {
1213
+ const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1214
+ if (!bearerToken) {
1215
+ throw new Error('API_TOKEN environment variable not found');
1216
+ }
1217
+
1218
+ const config = {
1219
+ method: 'get',
1220
+ url,
1221
+ headers: {
1222
+ ...this.headers,
1223
+ 'Authorization': `Bearer ${bearerToken}`
1224
+ },
1225
+ timeout: 60000, // 60s
1226
+ validateStatus: function (status) {
1227
+ return status < 500;
1228
+ }
1229
+ };
1230
+
1231
+ const response = await axios(config);
1232
+
1233
+ if (response.status !== 200) {
1234
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1235
+ }
1236
+
1237
+ this.log('info', `Successfully fetched build settings for ${domain}`);
1238
+ return response.data;
1239
+
1240
+ } catch (error) {
1241
+ const errorMessage = error instanceof Error ? error.message : String(error);
1242
+ this.log('error', `Failed to fetch build settings: ${errorMessage}`);
1243
+
1244
+ if (axios.isAxiosError(error)) {
1245
+ const responseData = error.response?.data;
1246
+ const responseStatus = error.response?.status;
1247
+ this.log('error', 'API Error Details:', {
1248
+ status: responseStatus,
1249
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1250
+ });
1251
+ }
1252
+
1253
+ throw error;
1254
+ }
1255
+ }
1256
+
1257
+ async hosting_deployJsApplication_triggerBuild(username, domain, archivePath, buildSettings) {
1258
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1259
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
1260
+
1261
+ try {
1262
+ const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1263
+ if (!bearerToken) {
1264
+ throw new Error('API_TOKEN environment variable not found');
1265
+ }
1266
+
1267
+ const archiveBasename = path.basename(archivePath);
1268
+ const buildData = {
1269
+ ...buildSettings,
1270
+ node_version: buildSettings?.node_version || 20,
1271
+ source_type: 'archive',
1272
+ source_options: {
1273
+ archive_path: archiveBasename
1274
+ }
1275
+ };
1276
+
1277
+ const config = {
1278
+ method: 'post',
1279
+ url,
1280
+ headers: {
1281
+ ...this.headers,
1282
+ 'Authorization': `Bearer ${bearerToken}`,
1283
+ 'Content-Type': 'application/json'
1284
+ },
1285
+ data: buildData,
1286
+ timeout: 60000, // 60s
1287
+ validateStatus: function (status) {
1288
+ return status < 500;
1289
+ }
1290
+ };
1291
+
1292
+ const response = await axios(config);
1293
+
1294
+ if (response.status !== 200) {
1295
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1296
+ }
1297
+
1298
+ this.log('info', `Successfully triggered build for ${domain}`);
1299
+ return response.data;
1300
+
1301
+ } catch (error) {
1302
+ const errorMessage = error instanceof Error ? error.message : String(error);
1303
+ this.log('error', `Failed to trigger build: ${errorMessage}`);
1304
+
1305
+ if (axios.isAxiosError(error)) {
1306
+ const responseData = error.response?.data;
1307
+ const responseStatus = error.response?.status;
1308
+ this.log('error', 'API Error Details:', {
1309
+ status: responseStatus,
1310
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1311
+ });
1312
+ }
1313
+
1314
+ throw error;
1315
+ }
1316
+ }
1317
+
1318
+ async handleJavascriptApplicationDeploy(params) {
1319
+ const { domain, archivePath, removeArchive = false } = params;
1320
+
1321
+ this.hosting_deployJsApplication_validateRequiredParams(params);
1322
+ this.hosting_deployJsApplication_validateArchiveFile(archivePath);
1323
+
1324
+ // Auto-resolve username from domain
1325
+ this.log('info', `Resolving username from domain: ${domain}`);
1326
+ const username = await this.resolveUsername(domain);
1327
+
1328
+ // Upload archive file
1329
+ this.log('info', `Starting archive upload for ${domain}`);
1330
+
1331
+ let uploadCredentials;
1332
+ try {
1333
+ uploadCredentials = await this.fetchUploadCredentials(username, domain);
1334
+ } catch (error) {
1335
+ const errorMessage = error instanceof Error ? error.message : String(error);
1336
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
1337
+ }
1338
+
1339
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
1340
+
1341
+ if (!uploadUrl || !authToken || !authRestToken) {
1342
+ throw new Error('Invalid upload credentials received from API');
1343
+ }
1344
+
1345
+ const archiveBasename = path.basename(archivePath);
1346
+ let uploadResult;
1347
+ try {
1348
+ const stats = fs.statSync(archivePath);
1349
+ uploadResult = await this.uploadFile(
1350
+ archivePath,
1351
+ archiveBasename,
1352
+ uploadUrl,
1353
+ authRestToken,
1354
+ authToken
1355
+ );
1356
+
1357
+ this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
1358
+ } catch (error) {
1359
+ const errorMessage = error instanceof Error ? error.message : String(error);
1360
+ throw new Error(`Failed to upload archive: ${errorMessage}`);
1361
+ }
1362
+
1363
+ // Fetch build settings
1364
+ let buildSettings;
1365
+ try {
1366
+ this.log('info', `Fetching build settings for ${domain}`);
1367
+ buildSettings = await this.hosting_deployJsApplication_fetchBuildSettings(username, domain, archivePath);
1368
+ } catch (error) {
1369
+ const errorMessage = error instanceof Error ? error.message : String(error);
1370
+ this.log('error', `Failed to fetch build settings: ${errorMessage}`);
1371
+ const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
1372
+
1373
+ return {
1374
+ upload: {
1375
+ status: 'success',
1376
+ data: {
1377
+ filename: uploadResult.filename
1378
+ }
1379
+ },
1380
+ resolveSettings: {
1381
+ status: 'error',
1382
+ error: errorMessage
1383
+ },
1384
+ build: {
1385
+ status: 'skipped'
1386
+ },
1387
+ removeArchive: {
1388
+ status: archiveRemoved ? 'success' : 'skipped'
1389
+ }
1390
+ };
1391
+ }
1392
+
1393
+ // Trigger build
1394
+ let buildResult;
1395
+ try {
1396
+ this.log('info', `Triggering build for ${domain}`);
1397
+ buildResult = await this.hosting_deployJsApplication_triggerBuild(username, domain, archivePath, buildSettings);
1398
+ } catch (error) {
1399
+ const errorMessage = error instanceof Error ? error.message : String(error);
1400
+ this.log('error', `Failed to trigger build: ${errorMessage}`);
1401
+ const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
1402
+
1403
+ return {
1404
+ upload: {
1405
+ status: 'success',
1406
+ data: {
1407
+ filename: uploadResult.filename
1408
+ }
1409
+ },
1410
+ resolveSettings: {
1411
+ status: 'success',
1412
+ data: buildSettings
1413
+ },
1414
+ build: {
1415
+ status: 'error',
1416
+ error: errorMessage
1417
+ },
1418
+ removeArchive: {
1419
+ status: archiveRemoved ? 'success' : 'skipped'
1420
+ }
1421
+ };
1422
+ }
1423
+
1424
+ const archiveRemoved = this.hosting_deployJsApplication_removeArchive(archivePath, removeArchive);
1425
+
1426
+ return {
1427
+ upload: {
1428
+ status: 'success',
1429
+ data: {
1430
+ filename: uploadResult.filename
1431
+ }
1432
+ },
1433
+ resolveSettings: {
1434
+ status: 'success',
1435
+ data: buildSettings
1436
+ },
1437
+ build: {
1438
+ status: 'success',
1439
+ data: buildResult
1440
+ },
1441
+ removeArchive: {
1442
+ status: archiveRemoved ? 'success' : 'skipped'
1443
+ }
1444
+ };
1445
+ }
1446
+
1447
+ hosting_deployStaticWebsite_validateArchiveFormat(filePath) {
1448
+ const validExtensions = ['zip', 'tar', 'tar.gz', 'tgz', '7z', 'gz', 'gzip'];
1449
+ const fileName = path.basename(filePath).toLowerCase();
1450
+
1451
+ for (const ext of validExtensions) {
1452
+ if (fileName.endsWith(`.${ext}`)) {
1453
+ return true;
1454
+ }
1455
+ }
1456
+
1457
+ return false;
1458
+ }
1459
+
1460
+ hosting_deployStaticWebsite_validateRequiredParams(params) {
1461
+ const { domain, archivePath, removeArchive } = params;
1462
+
1463
+ if (!domain || typeof domain !== 'string') {
1464
+ throw new Error('domain is required and must be a string');
1465
+ }
1466
+
1467
+ if (!archivePath || typeof archivePath !== 'string') {
1468
+ throw new Error('archivePath is required and must be a string');
1469
+ }
1470
+
1471
+ if (removeArchive !== undefined && typeof removeArchive !== 'boolean') {
1472
+ throw new Error('removeArchive must be a boolean if provided');
1473
+ }
1474
+ }
1475
+
1476
+ hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive) {
1477
+ if (!removeArchive) {
1478
+ return false;
1479
+ }
1480
+
1481
+ try {
1482
+ this.log('info', `Removing archive file: ${archivePath}`);
1483
+ fs.unlinkSync(archivePath);
1484
+ this.log('info', `Successfully removed archive file: ${archivePath}`);
1485
+ return true;
1486
+ } catch (error) {
1487
+ const errorMessage = error instanceof Error ? error.message : String(error);
1488
+ this.log('error', `Failed to remove archive file: ${errorMessage}`);
1489
+ return false;
1490
+ }
1491
+ }
1492
+
1493
+ hosting_deployStaticWebsite_validateArchiveFile(archivePath) {
1494
+ if (!fs.existsSync(archivePath)) {
1495
+ throw new Error(`Archive file not found: ${archivePath}`);
1496
+ }
1497
+
1498
+ const archiveStats = fs.statSync(archivePath);
1499
+ if (!archiveStats.isFile()) {
1500
+ throw new Error(`Archive path is not a file: ${archivePath}`);
1501
+ }
1502
+
1503
+ if (!this.hosting_deployStaticWebsite_validateArchiveFormat(archivePath)) {
1504
+ throw new Error('Invalid archive format. Supported formats: zip, tar, tar.gz, tgz, 7z, gz, gzip');
1505
+ }
1506
+ }
1507
+
1508
+ async hosting_deployStaticWebsite_triggerDeploy(username, domain, archivePath) {
1509
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1510
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/deploy`, baseUrl).toString();
1511
+
1512
+ try {
1513
+ const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1514
+ if (!bearerToken) {
1515
+ throw new Error('API_TOKEN environment variable not found');
1516
+ }
1517
+
1518
+ const archiveBasename = path.basename(archivePath);
1519
+ const deployData = {
1520
+ archive_path: archiveBasename
1521
+ };
1522
+
1523
+ const config = {
1524
+ method: 'post',
1525
+ url,
1526
+ headers: {
1527
+ ...this.headers,
1528
+ 'Authorization': `Bearer ${bearerToken}`,
1529
+ 'Content-Type': 'application/json'
1530
+ },
1531
+ data: deployData,
1532
+ timeout: 60000,
1533
+ validateStatus: function (status) {
1534
+ return status < 500;
1535
+ }
1536
+ };
1537
+
1538
+ const response = await axios(config);
1539
+
1540
+ if (response.status !== 200) {
1541
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1542
+ }
1543
+
1544
+ this.log('info', `Successfully triggered deployment for ${domain}`);
1545
+ return response.data;
1546
+
1547
+ } catch (error) {
1548
+ const errorMessage = error instanceof Error ? error.message : String(error);
1549
+ this.log('error', `Failed to trigger deployment: ${errorMessage}`);
1550
+
1551
+ if (axios.isAxiosError(error)) {
1552
+ const responseData = error.response?.data;
1553
+ const responseStatus = error.response?.status;
1554
+ this.log('error', 'API Error Details:', {
1555
+ status: responseStatus,
1556
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1557
+ });
1558
+ }
1559
+
1560
+ throw error;
1561
+ }
1562
+ }
1563
+
1564
+ async handleStaticWebsiteDeploy(params) {
1565
+ const { domain, archivePath, removeArchive = false } = params;
1566
+
1567
+ this.hosting_deployStaticWebsite_validateRequiredParams(params);
1568
+ this.hosting_deployStaticWebsite_validateArchiveFile(archivePath);
1569
+
1570
+ this.log('info', `Resolving username from domain: ${domain}`);
1571
+ const username = await this.resolveUsername(domain);
1572
+
1573
+ this.log('info', `Starting archive upload for ${domain}`);
1574
+
1575
+ let uploadCredentials;
1576
+ try {
1577
+ uploadCredentials = await this.fetchUploadCredentials(username, domain);
1578
+ } catch (error) {
1579
+ const errorMessage = error instanceof Error ? error.message : String(error);
1580
+ throw new Error(`Failed to fetch upload credentials: ${errorMessage}`);
1581
+ }
1582
+
1583
+ const { url: uploadUrl, auth_key: authToken, rest_auth_key: authRestToken } = uploadCredentials;
1584
+
1585
+ if (!uploadUrl || !authToken || !authRestToken) {
1586
+ throw new Error('Invalid upload credentials received from API');
1587
+ }
1588
+
1589
+ const archiveBasename = path.basename(archivePath);
1590
+ let uploadResult;
1591
+ try {
1592
+ const stats = fs.statSync(archivePath);
1593
+ uploadResult = await this.uploadFile(
1594
+ archivePath,
1595
+ archiveBasename,
1596
+ uploadUrl,
1597
+ authRestToken,
1598
+ authToken
1599
+ );
1600
+
1601
+ this.log('info', `Successfully uploaded archive: ${archiveBasename}`);
1602
+ } catch (error) {
1603
+ const errorMessage = error instanceof Error ? error.message : String(error);
1604
+ throw new Error(`Failed to upload archive: ${errorMessage}`);
1605
+ }
1606
+
1607
+ let deployResult;
1608
+ try {
1609
+ this.log('info', `Triggering deployment for ${domain}`);
1610
+ deployResult = await this.hosting_deployStaticWebsite_triggerDeploy(username, domain, archivePath);
1611
+ } catch (error) {
1612
+ const errorMessage = error instanceof Error ? error.message : String(error);
1613
+ this.log('error', `Failed to trigger deployment: ${errorMessage}`);
1614
+ const archiveRemoved = this.hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive);
1615
+
1616
+ return {
1617
+ upload: {
1618
+ status: 'success',
1619
+ data: {
1620
+ filename: uploadResult.filename
1621
+ }
1622
+ },
1623
+ deploy: {
1624
+ status: 'error',
1625
+ error: errorMessage
1626
+ },
1627
+ removeArchive: {
1628
+ status: archiveRemoved ? 'success' : 'skipped'
1629
+ }
1630
+ };
1631
+ }
1632
+
1633
+ const archiveRemoved = this.hosting_deployStaticWebsite_removeArchive(archivePath, removeArchive);
1634
+
1635
+ return {
1636
+ upload: {
1637
+ status: 'success',
1638
+ data: {
1639
+ filename: uploadResult.filename
1640
+ }
1641
+ },
1642
+ deploy: {
1643
+ status: 'success',
1644
+ data: deployResult
1645
+ },
1646
+ removeArchive: {
1647
+ status: archiveRemoved ? 'success' : 'skipped'
1648
+ }
1649
+ };
1650
+ }
1651
+
1652
+ hosting_listJsDeployments_validateRequiredParams(params) {
1653
+ const { domain } = params;
1654
+
1655
+ if (!domain || typeof domain !== 'string') {
1656
+ throw new Error('domain is required and must be a string');
1657
+ }
1658
+ }
1659
+
1660
+ hosting_listJsDeployments_buildQueryParams(params) {
1661
+ const { page, perPage, states } = params;
1662
+ const queryParams = new URLSearchParams();
1663
+
1664
+ if (page !== undefined && page !== null) {
1665
+ queryParams.append('page', page.toString());
1666
+ }
1667
+
1668
+ if (perPage !== undefined && perPage !== null) {
1669
+ queryParams.append('per_page', perPage.toString());
1670
+ }
1671
+
1672
+ if (states && Array.isArray(states) && states.length > 0) {
1673
+ states.forEach(state => {
1674
+ queryParams.append('states[]', state);
1675
+ });
1676
+ }
1677
+
1678
+ return queryParams.toString();
1679
+ }
1680
+
1681
+ async hosting_listJsDeployments_fetchDeployments(username, domain, queryParams) {
1682
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1683
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
1684
+
1685
+ const fullUrl = queryParams ? `${url}?${queryParams}` : url;
1686
+
1687
+ try {
1688
+ const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1689
+ if (!bearerToken) {
1690
+ throw new Error('API_TOKEN environment variable not found');
1691
+ }
1692
+
1693
+ const config = {
1694
+ method: 'get',
1695
+ url: fullUrl,
1696
+ headers: {
1697
+ ...this.headers,
1698
+ 'Authorization': `Bearer ${bearerToken}`
1699
+ },
1700
+ timeout: 60000, // 60s
1701
+ validateStatus: function (status) {
1702
+ return status < 500;
1703
+ }
1704
+ };
1705
+
1706
+ const response = await axios(config);
1707
+
1708
+ if (response.status !== 200) {
1709
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1710
+ }
1711
+
1712
+ this.log('info', `Successfully fetched deployments for ${domain}`);
1713
+ return response.data;
1714
+
1715
+ } catch (error) {
1716
+ const errorMessage = error instanceof Error ? error.message : String(error);
1717
+ this.log('error', `Failed to fetch deployments: ${errorMessage}`);
1718
+
1719
+ if (axios.isAxiosError(error)) {
1720
+ const responseData = error.response?.data;
1721
+ const responseStatus = error.response?.status;
1722
+ this.log('error', 'API Error Details:', {
1723
+ status: responseStatus,
1724
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1725
+ });
1726
+ }
1727
+
1728
+ throw error;
1729
+ }
1730
+ }
1731
+
1732
+ async handleListJavascriptDeployments(params) {
1733
+ const { domain, page, perPage, states } = params;
1734
+
1735
+ this.hosting_listJsDeployments_validateRequiredParams(params);
1736
+
1737
+ // Auto-resolve username from domain
1738
+ this.log('info', `Resolving username from domain: ${domain}`);
1739
+ const username = await this.resolveUsername(domain);
1740
+
1741
+ // Build query parameters
1742
+ const queryParams = this.hosting_listJsDeployments_buildQueryParams(params);
1743
+
1744
+ // Fetch deployments
1745
+ let deployments;
1746
+ try {
1747
+ this.log('info', `Fetching deployments for ${domain}`);
1748
+ deployments = await this.hosting_listJsDeployments_fetchDeployments(username, domain, queryParams);
1749
+ } catch (error) {
1750
+ const errorMessage = error instanceof Error ? error.message : String(error);
1751
+ this.log('error', `Failed to fetch deployments: ${errorMessage}`);
1752
+ throw error;
1753
+ }
1754
+
1755
+ return {
1756
+ status: 'success',
1757
+ domain,
1758
+ username,
1759
+ queryParams: {
1760
+ page,
1761
+ perPage,
1762
+ states
1763
+ },
1764
+ deployments
1765
+ };
1766
+ }
1767
+
1768
+ hosting_showJsDeploymentLogs_validateRequiredParams(params) {
1769
+ const { domain, buildUuid, fromLine } = params;
1770
+
1771
+ if (!domain || typeof domain !== 'string') {
1772
+ throw new Error('domain is required and must be a string');
1773
+ }
1774
+
1775
+ if (!buildUuid || typeof buildUuid !== 'string') {
1776
+ throw new Error('buildUuid is required and must be a string');
1777
+ }
1778
+
1779
+ if (fromLine !== undefined && (typeof fromLine !== 'number' || !Number.isInteger(fromLine) || fromLine < 0)) {
1780
+ throw new Error('fromLine must be a non-negative integer when provided');
1781
+ }
1782
+ }
1783
+
1784
+ hosting_showJsDeploymentLogs_buildQueryParams(params) {
1785
+ const { fromLine } = params;
1786
+ const queryParams = new URLSearchParams();
1787
+
1788
+ const line = (typeof fromLine === 'number' && Number.isInteger(fromLine) && fromLine >= 0) ? fromLine : 0;
1789
+ queryParams.append('from_line', line.toString());
1790
+
1791
+ return queryParams.toString();
1792
+ }
1793
+
1794
+ async hosting_showJsDeploymentLogs_fetchLogs(username, domain, buildUuid, queryParams) {
1795
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1796
+ const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/${buildUuid}/logs`, baseUrl).toString();
1797
+
1798
+ const fullUrl = queryParams ? `${url}?${queryParams}` : url;
1799
+
1800
+ try {
1801
+ const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1802
+ if (!bearerToken) {
1803
+ throw new Error('API_TOKEN environment variable not found');
1804
+ }
1805
+
1806
+ const config = {
1807
+ method: 'get',
1808
+ url: fullUrl,
1809
+ headers: {
1810
+ ...this.headers,
1811
+ 'Authorization': `Bearer ${bearerToken}`
1812
+ },
1813
+ timeout: 60000,
1814
+ validateStatus: function (status) {
1815
+ return status < 500;
1816
+ }
1817
+ };
1818
+
1819
+ const response = await axios(config);
1820
+
1821
+ if (response.status !== 200) {
1822
+ throw new Error(`API returned status ${response.status}: ${JSON.stringify(response.data)}`);
1823
+ }
1824
+
1825
+ this.log('info', `Successfully fetched logs for ${domain} build ${buildUuid}`);
1826
+ return response.data;
1827
+
1828
+ } catch (error) {
1829
+ const errorMessage = error instanceof Error ? error.message : String(error);
1830
+ this.log('error', `Failed to fetch logs: ${errorMessage}`);
1831
+
1832
+ if (axios.isAxiosError(error)) {
1833
+ const responseData = error.response?.data;
1834
+ const responseStatus = error.response?.status;
1835
+ this.log('error', 'API Error Details:', {
1836
+ status: responseStatus,
1837
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1838
+ });
1839
+ }
1840
+
1841
+ throw error;
1842
+ }
1843
+ }
1844
+
1845
+ async handleShowJsDeploymentLogs(params) {
1846
+ const { domain, buildUuid, fromLine } = params;
1847
+
1848
+ this.hosting_showJsDeploymentLogs_validateRequiredParams(params);
1849
+
1850
+ this.log('info', `Resolving username from domain: ${domain}`);
1851
+ const username = await this.resolveUsername(domain);
1852
+
1853
+ const queryParams = this.hosting_showJsDeploymentLogs_buildQueryParams(params);
1854
+
1855
+ let logs;
1856
+ try {
1857
+ this.log('info', `Fetching logs for ${domain}, build ${buildUuid}`);
1858
+ logs = await this.hosting_showJsDeploymentLogs_fetchLogs(username, domain, buildUuid, queryParams);
1859
+ } catch (error) {
1860
+ const errorMessage = error instanceof Error ? error.message : String(error);
1861
+ this.log('error', `Failed to fetch logs: ${errorMessage}`);
1862
+ throw error;
1863
+ }
1864
+
1865
+ const effectiveFromLine = (typeof fromLine === 'number' && Number.isInteger(fromLine) && fromLine >= 0) ? fromLine : 0;
1866
+
1867
+ return {
1868
+ domain,
1869
+ username,
1870
+ buildUuid,
1871
+ fromLine: effectiveFromLine,
1872
+ logs
1873
+ };
1874
+ }
1875
+
1876
+ /**
1877
+ * Execute an API call for a tool
1878
+ */
1879
+ async executeApiCall(tool, params) {
1880
+ // Get method and path from tool
1881
+ const method = tool.method;
1882
+ let path = tool.path;
1883
+
1884
+ // Clone params to avoid modifying the original
1885
+ const requestParams = { ...params };
1886
+
1887
+ // Replace path parameters with values from params
1888
+ Object.entries(requestParams).forEach(([key, value]) => {
1889
+ const placeholder = `{${key}}`;
1890
+ if (path.includes(placeholder)) {
1891
+ path = path.replace(placeholder, encodeURIComponent(String(value)));
1892
+ delete requestParams[key]; // Remove used parameter
1893
+ }
1894
+ });
1895
+
1896
+ // Build the full URL
1897
+ const baseUrl = this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`;
1898
+ const cleanPath = path.startsWith("/") ? path.slice(1) : path;
1899
+ const url = new URL(cleanPath, baseUrl).toString();
1900
+
1901
+ this.log('debug', `API Request: ${method} ${url}`);
1902
+
1903
+ try {
1904
+ // Configure the request
1905
+ const config = {
1906
+ method: method.toLowerCase(),
1907
+ url,
1908
+ headers: { ...this.headers },
1909
+ timeout: 60000, // 60s
1910
+ validateStatus: function (status) {
1911
+ return status < 500; // Resolve only if the status code is less than 500
1912
+ }
1913
+ };
1914
+
1915
+ const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN']; // APITOKEN for backwards compatibility
1916
+ if (bearerToken) {
1917
+ config.headers['Authorization'] = `Bearer ${bearerToken}`;
1918
+ } else {
1919
+ this.log('error', `Bearer Token environment variable not found: API_TOKEN`);
1920
+ }
1921
+
1922
+ // Add parameters based on request method
1923
+ if (["GET", "DELETE"].includes(method)) {
1924
+ // For GET/DELETE, send params as query string
1925
+ config.params = { ...(config.params || {}), ...requestParams };
1926
+ } else {
1927
+ // For POST/PUT/PATCH, send params as JSON body
1928
+ config.data = requestParams;
1929
+ config.headers["Content-Type"] = "application/json";
1930
+ }
1931
+
1932
+ this.log('debug', "Request config:", {
1933
+ url: config.url,
1934
+ method: config.method,
1935
+ params: config.params,
1936
+ headers: Object.keys(config.headers)
1937
+ });
1938
+
1939
+ // Execute the request
1940
+ const response = await axios(config);
1941
+ this.log('debug', `Response status: ${response.status}`);
1942
+
1943
+ return response.data;
1944
+
1945
+ } catch (error) {
1946
+ const errorMessage = error instanceof Error ? error.message : String(error);
1947
+ this.log('error', `API request failed: ${errorMessage}`);
1948
+
1949
+ if (axios.isAxiosError(error)) {
1950
+ const responseData = error.response?.data;
1951
+ const responseStatus = error.response?.status;
1952
+
1953
+ this.log('error', 'API Error Details:', {
1954
+ status: responseStatus,
1955
+ data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
1956
+ });
1957
+
1958
+ // Rethrow with more context for better error handling
1959
+ const detailedError = new Error(`API request failed with status ${responseStatus}: ${errorMessage}`);
1960
+ detailedError.response = error.response;
1961
+ throw detailedError;
1962
+ }
1963
+
1964
+ throw error;
1965
+ }
1966
+ }
1967
+
1968
+ /**
1969
+ * Log messages with appropriate level
1970
+ * Only sends to MCP if we're connected
1971
+ */
1972
+ log(level, message, data) {
1973
+ // Always log to stderr for visibility
1974
+ console.error(`[${level.toUpperCase()}] ${message}${data ? ': ' + JSON.stringify(data) : ''}`);
1975
+
1976
+ // Only try to send via MCP if we're in debug mode or it's important
1977
+ if (this.debug || level !== 'debug') {
1978
+ try {
1979
+ // Only send if server exists and is connected
1980
+ if (this.server && this.server.isConnected) {
1981
+ this.server.sendLoggingMessage({
1982
+ level,
1983
+ data: `[MCP Server] ${message}${data ? ': ' + JSON.stringify(data) : ''}`
1984
+ });
1985
+ }
1986
+ } catch (e) {
1987
+ // If logging fails, log to stderr
1988
+ console.error('Failed to send log via MCP:', e.message);
1989
+ }
1990
+ }
1991
+ }
1992
+
1993
+ /**
1994
+ * Create and configure Express app with shared middleware
1995
+ */
1996
+ createApp() {
1997
+ const app = express();
1998
+ app.use(express.json());
1999
+ app.use(cors());
2000
+ return app;
2001
+ }
2002
+
2003
+ /**
2004
+ * Start the server with HTTP streaming transport
2005
+ */
2006
+ async startHttp(host, port) {
2007
+ try {
2008
+ const app = this.createApp();
2009
+
2010
+ // Create HTTP transport with session management
2011
+ const httpTransport = new StreamableHTTPServerTransport({
2012
+ sessionIdGenerator: () => {
2013
+ // Generate a simple session ID
2014
+ return `session-${Date.now()}-${Math.random().toString(36).substring(7)}`;
2015
+ },
2016
+ });
2017
+
2018
+ // Set up CORS for all routes
2019
+ app.options("*", (req, res) => {
2020
+ res.header("Access-Control-Allow-Origin", "*");
2021
+ res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
2022
+ res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, x-session-id");
2023
+ res.sendStatus(200);
2024
+ });
2025
+
2026
+ // Health check endpoint
2027
+ app.get("/health", (req, res) => {
2028
+ res.status(200).json({ status: "ok", transport: "http" });
2029
+ });
2030
+
2031
+ // Set up the HTTP transport endpoint
2032
+ app.post("/", async (req, res) => {
2033
+ res.header("Access-Control-Allow-Origin", "*");
2034
+ res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
2035
+ res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, x-session-id");
2036
+ await httpTransport.handleRequest(req, res, req.body);
2037
+ });
2038
+
2039
+ // Start the server
2040
+ const server = app.listen(port, host, () => {
2041
+ this.log('info', `MCP Server with HTTP streaming transport started successfully with ${this.tools.size} tools`);
2042
+ this.log('info', `Listening on http://${host}:${port}`);
2043
+ });
2044
+
2045
+ // Connect the MCP server to the transport
2046
+ await this.server.connect(httpTransport);
2047
+
2048
+ } catch (error) {
2049
+ console.error("Failed to start MCP server:", error);
2050
+ process.exit(1);
2051
+ }
2052
+ }
2053
+
2054
+ /**
2055
+ * Start the server
2056
+ */
2057
+ async startStdio() {
2058
+ try {
2059
+ // Create stdio transport
2060
+ const transport = new StdioServerTransport();
2061
+ console.error("MCP Server starting on stdio transport");
2062
+
2063
+ // Connect to the transport
2064
+ await this.server.connect(transport);
2065
+
2066
+ // Now we can safely log via MCP
2067
+ console.error(`Registered ${this.tools.size} tools`);
2068
+ this.log('info', `MCP Server with stdio transport started successfully with ${this.tools.size} tools`);
2069
+ } catch (error) {
2070
+ console.error("Failed to start MCP server:", error);
2071
+ process.exit(1);
2072
+ }
2073
+ }
2074
+ }
2075
+
2076
+ export async function startServer({ name, version, tools }) {
2077
+ const argv = minimist(process.argv.slice(2), {
2078
+ string: ['host'],
2079
+ boolean: ['stdio', 'http', 'help'],
2080
+ default: { host: '127.0.0.1', port: 8100, stdio: true }
2081
+ });
2082
+
2083
+ if (argv.help) {
2084
+ console.log(`
2085
+ ${name}
2086
+ Usage: ${name} [options]
2087
+ Options:
2088
+ --http Use HTTP streaming transport
2089
+ --stdio Use standard input/output transport (default)
2090
+ --host <host> Host to bind to (default: 127.0.0.1)
2091
+ --port <port> Port to bind to (default: 8100)
2092
+ --help Show this help message
2093
+ Environment Variables:
2094
+ API_TOKEN Your Hostinger API token (required)
2095
+ DEBUG Enable debug logging (true/false)
2096
+ `);
2097
+ process.exit(0);
2098
+ }
2099
+
2100
+ const server = new MCPServer({ name, version, tools });
2101
+ if (argv.http) {
2102
+ await server.startHttp(argv.host, argv.port);
2103
+ } else {
2104
+ await server.startStdio();
2105
+ }
2106
+ }