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.
- package/.env.example +4 -0
- package/README.md +40 -2
- package/package.json +1 -1
- package/src/core/oauth-error.html +90 -0
- package/src/core/oauth-success.html +77 -0
- package/src/core/oauth.js +392 -0
- package/src/core/oauth.ts +362 -0
- package/src/core/runtime.js +71 -55
- package/src/core/runtime.ts +77 -55
- package/src/servers/all.js +1 -1
- package/src/servers/all.ts +1 -1
- package/src/servers/billing.js +1 -1
- package/src/servers/billing.ts +1 -1
- package/src/servers/dns.js +1 -1
- package/src/servers/dns.ts +1 -1
- package/src/servers/domains.js +1 -1
- package/src/servers/domains.ts +1 -1
- package/src/servers/hosting.js +1 -1
- package/src/servers/hosting.ts +1 -1
- package/src/servers/reach.js +1 -1
- package/src/servers/reach.ts +1 -1
- package/src/servers/vps.js +1 -1
- package/src/servers/vps.ts +1 -1
package/src/core/runtime.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
1946
|
-
|
|
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
|
-
|
|
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
|
|
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);
|
package/src/servers/all.js
CHANGED
package/src/servers/all.ts
CHANGED
package/src/servers/billing.js
CHANGED
package/src/servers/billing.ts
CHANGED
package/src/servers/dns.js
CHANGED
package/src/servers/dns.ts
CHANGED
package/src/servers/domains.js
CHANGED
package/src/servers/domains.ts
CHANGED
package/src/servers/hosting.js
CHANGED
package/src/servers/hosting.ts
CHANGED
package/src/servers/reach.js
CHANGED
package/src/servers/reach.ts
CHANGED
package/src/servers/vps.js
CHANGED
package/src/servers/vps.ts
CHANGED