hostinger-api-mcp 0.1.43 → 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();
@@ -108,6 +111,14 @@ class MCPServer {
108
111
  return headers;
109
112
  }
110
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
+
111
122
  /**
112
123
  * Initialize tools map from OpenAPI spec
113
124
  * This runs before the server is connected, so don't log here
@@ -228,10 +239,7 @@ class MCPServer {
228
239
  const url = new URL(`api/hosting/v1/websites?domain=${encodeURIComponent(domain)}`, baseUrl).toString();
229
240
 
230
241
  try {
231
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
232
- if (!bearerToken) {
233
- throw new Error('API_TOKEN environment variable not found');
234
- }
242
+ const bearerToken = await this.getAuthToken();
235
243
 
236
244
  const config: AxiosRequestConfig = {
237
245
  method: 'get',
@@ -287,10 +295,7 @@ class MCPServer {
287
295
  const url = new URL('api/hosting/v1/files/upload-urls', baseUrl).toString();
288
296
 
289
297
  try {
290
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
291
- if (!bearerToken) {
292
- throw new Error('API_TOKEN environment variable not found');
293
- }
298
+ const bearerToken = await this.getAuthToken();
294
299
 
295
300
  const config: AxiosRequestConfig = {
296
301
  method: 'post',
@@ -490,10 +495,7 @@ class MCPServer {
490
495
  const url = new URL(`api/hosting/v1/accounts/${username}/domains/${domain}/is-empty`, baseUrl).toString();
491
496
 
492
497
  try {
493
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
494
- if (!bearerToken) {
495
- throw new Error('API_TOKEN environment variable not found');
496
- }
498
+ const bearerToken = await this.getAuthToken();
497
499
 
498
500
  const config: AxiosRequestConfig = {
499
501
  method: 'get',
@@ -545,10 +547,7 @@ class MCPServer {
545
547
  const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/import`, baseUrl).toString();
546
548
 
547
549
  try {
548
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
549
- if (!bearerToken) {
550
- throw new Error('API_TOKEN environment variable not found');
551
- }
550
+ const bearerToken = await this.getAuthToken();
552
551
 
553
552
  const config: AxiosRequestConfig = {
554
553
  method: 'post',
@@ -773,10 +772,7 @@ class MCPServer {
773
772
  const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/plugins/deploy`, baseUrl).toString();
774
773
 
775
774
  try {
776
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
777
- if (!bearerToken) {
778
- throw new Error('API_TOKEN environment variable not found');
779
- }
775
+ const bearerToken = await this.getAuthToken();
780
776
 
781
777
  const config: AxiosRequestConfig = {
782
778
  method: 'post',
@@ -1007,10 +1003,7 @@ class MCPServer {
1007
1003
  const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/wordpress/themes/deploy`, baseUrl).toString();
1008
1004
 
1009
1005
  try {
1010
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1011
- if (!bearerToken) {
1012
- throw new Error('API_TOKEN environment variable not found');
1013
- }
1006
+ const bearerToken = await this.getAuthToken();
1014
1007
 
1015
1008
  const config: AxiosRequestConfig = {
1016
1009
  method: 'post',
@@ -1244,10 +1237,7 @@ class MCPServer {
1244
1237
  const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds/settings/from-archive?archive_path=${encodeURIComponent(archiveBasename)}`, baseUrl).toString();
1245
1238
 
1246
1239
  try {
1247
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1248
- if (!bearerToken) {
1249
- throw new Error('API_TOKEN environment variable not found');
1250
- }
1240
+ const bearerToken = await this.getAuthToken();
1251
1241
 
1252
1242
  const config: AxiosRequestConfig = {
1253
1243
  method: 'get',
@@ -1293,10 +1283,7 @@ class MCPServer {
1293
1283
  const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/nodejs/builds`, baseUrl).toString();
1294
1284
 
1295
1285
  try {
1296
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1297
- if (!bearerToken) {
1298
- throw new Error('API_TOKEN environment variable not found');
1299
- }
1286
+ const bearerToken = await this.getAuthToken();
1300
1287
 
1301
1288
  const archiveBasename = path.basename(archivePath);
1302
1289
  const buildData = {
@@ -1544,10 +1531,7 @@ class MCPServer {
1544
1531
  const url = new URL(`api/hosting/v1/accounts/${username}/websites/${domain}/deploy`, baseUrl).toString();
1545
1532
 
1546
1533
  try {
1547
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1548
- if (!bearerToken) {
1549
- throw new Error('API_TOKEN environment variable not found');
1550
- }
1534
+ const bearerToken = await this.getAuthToken();
1551
1535
 
1552
1536
  const archiveBasename = path.basename(archivePath);
1553
1537
  const deployData = {
@@ -1719,10 +1703,7 @@ class MCPServer {
1719
1703
  const fullUrl = queryParams ? `${url}?${queryParams}` : url;
1720
1704
 
1721
1705
  try {
1722
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1723
- if (!bearerToken) {
1724
- throw new Error('API_TOKEN environment variable not found');
1725
- }
1706
+ const bearerToken = await this.getAuthToken();
1726
1707
 
1727
1708
  const config: AxiosRequestConfig = {
1728
1709
  method: 'get',
@@ -1832,10 +1813,7 @@ class MCPServer {
1832
1813
  const fullUrl = queryParams ? `${url}?${queryParams}` : url;
1833
1814
 
1834
1815
  try {
1835
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1836
- if (!bearerToken) {
1837
- throw new Error('API_TOKEN environment variable not found');
1838
- }
1816
+ const bearerToken = await this.getAuthToken();
1839
1817
 
1840
1818
  const config: AxiosRequestConfig = {
1841
1819
  method: 'get',
@@ -1946,11 +1924,10 @@ class MCPServer {
1946
1924
  }
1947
1925
  };
1948
1926
 
1949
- const bearerToken = process.env['API_TOKEN'] || process.env['APITOKEN']; // APITOKEN for backwards compatibility
1950
- if (bearerToken && config.headers) {
1927
+ const envToken = process.env['API_TOKEN'] || process.env['APITOKEN'];
1928
+ let bearerToken = await this.getAuthToken();
1929
+ if (config.headers) {
1951
1930
  config.headers['Authorization'] = `Bearer ${bearerToken}`;
1952
- } else {
1953
- this.log('error', `Bearer Token environment variable not found: API_TOKEN`);
1954
1931
  }
1955
1932
 
1956
1933
  // Add parameters based on request method
@@ -1973,9 +1950,23 @@ class MCPServer {
1973
1950
  });
1974
1951
 
1975
1952
  // Execute the request
1976
- const response = await axios(config);
1953
+ let response = await axios(config);
1977
1954
  this.log('debug', `Response status: ${response.status}`);
1978
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
+
1979
1970
  return response.data;
1980
1971
 
1981
1972
  } catch (error) {
@@ -2113,7 +2104,7 @@ class MCPServer {
2113
2104
  export async function startServer({ name, version, tools }: { name: string; version: string; tools: OpenApiTool[] }): Promise<void> {
2114
2105
  const argv = minimist(process.argv.slice(2), {
2115
2106
  string: ['host'],
2116
- boolean: ['stdio', 'http', 'help'],
2107
+ boolean: ['stdio', 'http', 'help', 'login', 'logout'],
2117
2108
  default: { host: '127.0.0.1', port: 8100, stdio: true }
2118
2109
  });
2119
2110
  if (argv.help) {
@@ -2121,17 +2112,44 @@ export async function startServer({ name, version, tools }: { name: string; vers
2121
2112
  ${name}
2122
2113
  Usage: ${name} [options]
2123
2114
  Options:
2124
- --http Use HTTP streaming transport
2115
+ --http Use HTTP streaming transport (requires API_TOKEN env var)
2125
2116
  --stdio Use standard input/output transport (default)
2126
2117
  --host <host> Host to bind to (default: 127.0.0.1)
2127
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
2128
2121
  --help Show this help message
2129
2122
  Environment Variables:
2130
- 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)
2131
2125
  DEBUG Enable debug logging (true/false)
2132
2126
  `);
2133
2127
  process.exit(0);
2134
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
+
2135
2153
  const server = new MCPServer({ name, version, tools });
2136
2154
  if (argv.http) {
2137
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.43', 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.43', 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.43', 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.43', 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.43', 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.43', 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.43', 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.43', 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.43', 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.43', 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.43', 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.43', 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.43', 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.43', tools });
6
+ startServer({ name: 'hostinger-vps-mcp', version: '0.2.1', tools });