hostinger-api-mcp 0.1.42 → 0.2.1

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.
@@ -12,6 +12,7 @@ import {
12
12
  CallToolRequestSchema,
13
13
  Tool,
14
14
  } from "@modelcontextprotocol/sdk/types.js";
15
+ import { OAuthProvider } from "./oauth.js";
15
16
  import axios,{ AxiosRequestConfig, AxiosError, AxiosResponse } from "axios";
16
17
  import * as tus from "tus-js-client";
17
18
  import fs from "fs";
@@ -57,6 +58,7 @@ class MCPServer {
57
58
  private debug: boolean;
58
59
  private baseUrl: string;
59
60
  private headers: Record<string, string>;
61
+ private oauth: OAuthProvider;
60
62
 
61
63
  constructor({ name, version, tools }: { name: string; version: string; tools: OpenApiTool[] }) {
62
64
  // Initialize class properties
@@ -66,6 +68,7 @@ class MCPServer {
66
68
  this.debug = process.env.DEBUG === "true";
67
69
  this.baseUrl = process.env.API_BASE_URL || "https://developers.hostinger.com";
68
70
  this.headers = this.parseHeaders(process.env.API_HEADERS || "");
71
+ this.oauth = new OAuthProvider();
69
72
 
70
73
  // Initialize tools map - do this before creating server
71
74
  this.initializeTools();
@@ -98,12 +101,24 @@ class MCPServer {
98
101
  if (key && value) headers[key.trim()] = value.trim();
99
102
  });
100
103
  }
101
-
102
- headers['User-Agent'] = `hostinger-mcp-server/${this.version}`;
103
-
104
+
105
+ const extensionUa = String(process.env.USER_AGENT ?? "")
106
+ .replace(/\r|\n/g, "")
107
+ .trim();
108
+ const base = `hostinger-mcp-server/${this.version}`;
109
+ headers["User-Agent"] = extensionUa ? `${base} (${extensionUa})` : base;
110
+
104
111
  return headers;
105
112
  }
106
113
 
114
+ /**
115
+ * Resolve a bearer token. API_TOKEN env var takes precedence; otherwise the
116
+ * OAuth provider handles login/refresh transparently.
117
+ */
118
+ private async getAuthToken(): Promise<string> {
119
+ return await this.oauth.getAccessToken();
120
+ }
121
+
107
122
  /**
108
123
  * Initialize tools map from OpenAPI spec
109
124
  * This runs before the server is connected, so don't log here
@@ -224,10 +239,7 @@ class MCPServer {
224
239
  const url = new URL(`api/hosting/v1/websites?domain=${encodeURIComponent(domain)}`, baseUrl).toString();
225
240
 
226
241
  try {
227
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
228
- if (!bearerToken) {
229
- throw new Error('API_TOKEN environment variable not found');
230
- }
242
+ const bearerToken = await this.getAuthToken();
231
243
 
232
244
  const config: AxiosRequestConfig = {
233
245
  method: 'get',
@@ -283,10 +295,7 @@ class MCPServer {
283
295
  const url = new URL('api/hosting/v1/files/upload-urls', baseUrl).toString();
284
296
 
285
297
  try {
286
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
287
- if (!bearerToken) {
288
- throw new Error('API_TOKEN environment variable not found');
289
- }
298
+ const bearerToken = await this.getAuthToken();
290
299
 
291
300
  const config: AxiosRequestConfig = {
292
301
  method: 'post',
@@ -486,10 +495,7 @@ class MCPServer {
486
495
  const url = new URL(`api/hosting/v1/accounts/${username}/domains/${domain}/is-empty`, baseUrl).toString();
487
496
 
488
497
  try {
489
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
490
- if (!bearerToken) {
491
- throw new Error('API_TOKEN environment variable not found');
492
- }
498
+ const bearerToken = await this.getAuthToken();
493
499
 
494
500
  const config: AxiosRequestConfig = {
495
501
  method: 'get',
@@ -541,10 +547,7 @@ class MCPServer {
541
547
  const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/import`, baseUrl).toString();
542
548
 
543
549
  try {
544
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
545
- if (!bearerToken) {
546
- throw new Error('API_TOKEN environment variable not found');
547
- }
550
+ const bearerToken = await this.getAuthToken();
548
551
 
549
552
  const config: AxiosRequestConfig = {
550
553
  method: 'post',
@@ -769,10 +772,7 @@ class MCPServer {
769
772
  const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/plugins/deploy`, baseUrl).toString();
770
773
 
771
774
  try {
772
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
773
- if (!bearerToken) {
774
- throw new Error('API_TOKEN environment variable not found');
775
- }
775
+ const bearerToken = await this.getAuthToken();
776
776
 
777
777
  const config: AxiosRequestConfig = {
778
778
  method: 'post',
@@ -1003,10 +1003,7 @@ class MCPServer {
1003
1003
  const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/themes/deploy`, baseUrl).toString();
1004
1004
 
1005
1005
  try {
1006
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1007
- if (!bearerToken) {
1008
- throw new Error('API_TOKEN environment variable not found');
1009
- }
1006
+ const bearerToken = await this.getAuthToken();
1010
1007
 
1011
1008
  const config: AxiosRequestConfig = {
1012
1009
  method: 'post',
@@ -1240,10 +1237,7 @@ class MCPServer {
1240
1237
  const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/settings/from-archive?archive_path=${encodeURIComponent(archiveBasename)}`, baseUrl).toString();
1241
1238
 
1242
1239
  try {
1243
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1244
- if (!bearerToken) {
1245
- throw new Error('API_TOKEN environment variable not found');
1246
- }
1240
+ const bearerToken = await this.getAuthToken();
1247
1241
 
1248
1242
  const config: AxiosRequestConfig = {
1249
1243
  method: 'get',
@@ -1289,10 +1283,7 @@ class MCPServer {
1289
1283
  const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
1290
1284
 
1291
1285
  try {
1292
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1293
- if (!bearerToken) {
1294
- throw new Error('API_TOKEN environment variable not found');
1295
- }
1286
+ const bearerToken = await this.getAuthToken();
1296
1287
 
1297
1288
  const archiveBasename = path.basename(archivePath);
1298
1289
  const buildData = {
@@ -1540,10 +1531,7 @@ class MCPServer {
1540
1531
  const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/deploy`, baseUrl).toString();
1541
1532
 
1542
1533
  try {
1543
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1544
- if (!bearerToken) {
1545
- throw new Error('API_TOKEN environment variable not found');
1546
- }
1534
+ const bearerToken = await this.getAuthToken();
1547
1535
 
1548
1536
  const archiveBasename = path.basename(archivePath);
1549
1537
  const deployData = {
@@ -1715,10 +1703,7 @@ class MCPServer {
1715
1703
  const fullUrl = queryParams ? `${url}?${queryParams}` : url;
1716
1704
 
1717
1705
  try {
1718
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1719
- if (!bearerToken) {
1720
- throw new Error('API_TOKEN environment variable not found');
1721
- }
1706
+ const bearerToken = await this.getAuthToken();
1722
1707
 
1723
1708
  const config: AxiosRequestConfig = {
1724
1709
  method: 'get',
@@ -1828,10 +1813,7 @@ class MCPServer {
1828
1813
  const fullUrl = queryParams ? `${url}?${queryParams}` : url;
1829
1814
 
1830
1815
  try {
1831
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1832
- if (!bearerToken) {
1833
- throw new Error('API_TOKEN environment variable not found');
1834
- }
1816
+ const bearerToken = await this.getAuthToken();
1835
1817
 
1836
1818
  const config: AxiosRequestConfig = {
1837
1819
  method: 'get',
@@ -1942,11 +1924,10 @@ class MCPServer {
1942
1924
  }
1943
1925
  };
1944
1926
 
1945
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN']; // APITOKEN for backwards compatibility
1946
- if (bearerToken && config.headers) {
1927
+ const envToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1928
+ let bearerToken = await this.getAuthToken();
1929
+ if (config.headers) {
1947
1930
  config.headers['Authorization'] = `Bearer ${bearerToken}`;
1948
- } else {
1949
- this.log('error', `Bearer Token environment variable not found: API_TOKEN`);
1950
1931
  }
1951
1932
 
1952
1933
  // Add parameters based on request method
@@ -1969,9 +1950,23 @@ class MCPServer {
1969
1950
  });
1970
1951
 
1971
1952
  // Execute the request
1972
- const response = await axios(config);
1953
+ let response = await axios(config);
1973
1954
  this.log('debug', `Response status: ${response.status}`);
1974
1955
 
1956
+ // Reactive token recovery: a 401 means the bearer was rejected even
1957
+ // though our local expiry said it was fine (e.g. revoked, account
1958
+ // changed, clock skew). Force a re-auth via refresh-or-login and retry
1959
+ // once. Skipped for the env-token path — nothing to refresh.
1960
+ if (response.status === 401 && !envToken) {
1961
+ this.log('info', 'API returned 401; reauthenticating and retrying once');
1962
+ bearerToken = await this.oauth.reauthenticate();
1963
+ if (config.headers) {
1964
+ config.headers['Authorization'] = `Bearer ${bearerToken}`;
1965
+ }
1966
+ response = await axios(config);
1967
+ this.log('debug', `Retry response status: ${response.status}`);
1968
+ }
1969
+
1975
1970
  return response.data;
1976
1971
 
1977
1972
  } catch (error) {
@@ -2109,7 +2104,7 @@ class MCPServer {
2109
2104
  export async function startServer({ name, version, tools }: { name: string; version: string; tools: OpenApiTool[] }): Promise<void> {
2110
2105
  const argv = minimist(process.argv.slice(2), {
2111
2106
  string: ['host'],
2112
- boolean: ['stdio', 'http', 'help'],
2107
+ boolean: ['stdio', 'http', 'help', 'login', 'logout'],
2113
2108
  default: { host: '127.0.0.1', port: 8100, stdio: true }
2114
2109
  });
2115
2110
  if (argv.help) {
@@ -2117,17 +2112,44 @@ export async function startServer({ name, version, tools }: { name: string; vers
2117
2112
  ${name}
2118
2113
  Usage: ${name} [options]
2119
2114
  Options:
2120
- --http Use HTTP streaming transport
2115
+ --http Use HTTP streaming transport (requires API_TOKEN env var)
2121
2116
  --stdio Use standard input/output transport (default)
2122
2117
  --host <host> Host to bind to (default: 127.0.0.1)
2123
2118
  --port <port> Port to bind to (default: 8100)
2119
+ --login Run OAuth sign-in flow and exit
2120
+ --logout Revoke stored OAuth credentials and exit
2124
2121
  --help Show this help message
2125
2122
  Environment Variables:
2126
- API_TOKEN Your Hostinger API token (required)
2123
+ API_TOKEN Hostinger API token (overrides OAuth when set)
2124
+ OAUTH_ISSUER OAuth server base URL (default: https://auth.hostinger.com)
2127
2125
  DEBUG Enable debug logging (true/false)
2128
2126
  `);
2129
2127
  process.exit(0);
2130
2128
  }
2129
+
2130
+ if (argv.login) {
2131
+ const provider = new OAuthProvider();
2132
+ console.error('[OAuth] Starting sign-in flow...');
2133
+ await provider.login();
2134
+ console.error('[OAuth] Sign-in successful. Credentials stored.');
2135
+ process.exit(0);
2136
+ }
2137
+
2138
+ if (argv.logout) {
2139
+ const provider = new OAuthProvider();
2140
+ await provider.logout();
2141
+ console.error('[OAuth] Signed out. Stored credentials revoked and cleared.');
2142
+ process.exit(0);
2143
+ }
2144
+
2145
+ if (argv.http) {
2146
+ const envToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
2147
+ if (!envToken) {
2148
+ console.error('[Error] HTTP transport requires the API_TOKEN environment variable. OAuth sign-in is only supported in stdio mode.');
2149
+ process.exit(1);
2150
+ }
2151
+ }
2152
+
2131
2153
  const server = new MCPServer({ name, version, tools });
2132
2154
  if (argv.http) {
2133
2155
  await server.startHttp(argv.host, argv.port);
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/all.js';
5
5
 
6
- startServer({ name: 'hostinger-api-mcp', version: '0.1.42', tools });
6
+ startServer({ name: 'hostinger-api-mcp', version: '0.2.1', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/all.js';
5
5
 
6
- startServer({ name: 'hostinger-api-mcp', version: '0.1.42', tools });
6
+ startServer({ name: 'hostinger-api-mcp', version: '0.2.1', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/billing.js';
5
5
 
6
- startServer({ name: 'hostinger-billing-mcp', version: '0.1.42', tools });
6
+ startServer({ name: 'hostinger-billing-mcp', version: '0.2.1', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/billing.js';
5
5
 
6
- startServer({ name: 'hostinger-billing-mcp', version: '0.1.42', tools });
6
+ startServer({ name: 'hostinger-billing-mcp', version: '0.2.1', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/dns.js';
5
5
 
6
- startServer({ name: 'hostinger-dns-mcp', version: '0.1.42', tools });
6
+ startServer({ name: 'hostinger-dns-mcp', version: '0.2.1', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/dns.js';
5
5
 
6
- startServer({ name: 'hostinger-dns-mcp', version: '0.1.42', tools });
6
+ startServer({ name: 'hostinger-dns-mcp', version: '0.2.1', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/domains.js';
5
5
 
6
- startServer({ name: 'hostinger-domains-mcp', version: '0.1.42', tools });
6
+ startServer({ name: 'hostinger-domains-mcp', version: '0.2.1', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/domains.js';
5
5
 
6
- startServer({ name: 'hostinger-domains-mcp', version: '0.1.42', tools });
6
+ startServer({ name: 'hostinger-domains-mcp', version: '0.2.1', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/hosting.js';
5
5
 
6
- startServer({ name: 'hostinger-hosting-mcp', version: '0.1.42', tools });
6
+ startServer({ name: 'hostinger-hosting-mcp', version: '0.2.1', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/hosting.js';
5
5
 
6
- startServer({ name: 'hostinger-hosting-mcp', version: '0.1.42', tools });
6
+ startServer({ name: 'hostinger-hosting-mcp', version: '0.2.1', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/reach.js';
5
5
 
6
- startServer({ name: 'hostinger-reach-mcp', version: '0.1.42', tools });
6
+ startServer({ name: 'hostinger-reach-mcp', version: '0.2.1', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/reach.js';
5
5
 
6
- startServer({ name: 'hostinger-reach-mcp', version: '0.1.42', tools });
6
+ startServer({ name: 'hostinger-reach-mcp', version: '0.2.1', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/vps.js';
5
5
 
6
- startServer({ name: 'hostinger-vps-mcp', version: '0.1.42', tools });
6
+ startServer({ name: 'hostinger-vps-mcp', version: '0.2.1', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/vps.js';
5
5
 
6
- startServer({ name: 'hostinger-vps-mcp', version: '0.1.42', tools });
6
+ startServer({ name: 'hostinger-vps-mcp', version: '0.2.1', tools });