hostinger-api-mcp 0.1.43 → 0.2.2
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 +43 -4
- 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 +406 -0
- package/src/core/oauth.ts +374 -0
- package/src/core/runtime.js +73 -60
- package/src/core/runtime.ts +77 -58
- package/src/core/tools/all.js +8 -0
- package/src/core/tools/all.ts +8 -0
- package/src/core/tools/reach.js +8 -0
- package/src/core/tools/reach.ts +8 -0
- 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/types.d.ts +8 -0
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, getEnvToken } 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";
|
|
@@ -46,7 +47,7 @@ const SECURITY_SCHEMES: Record<string, SecurityScheme> = {
|
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* MCP Server for Hostinger API
|
|
49
|
-
* Generated from OpenAPI spec version 0.
|
|
50
|
+
* Generated from OpenAPI spec version 0.12.0
|
|
50
51
|
*/
|
|
51
52
|
class MCPServer {
|
|
52
53
|
private readonly name: string;
|
|
@@ -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. HOSTINGER_API_TOKEN / API_TOKEN env vars take
|
|
116
|
+
* precedence; otherwise the 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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
1950
|
-
|
|
1927
|
+
const envToken = getEnvToken();
|
|
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
|
-
|
|
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,45 @@ export async function startServer({ name, version, tools }: { name: string; vers
|
|
|
2121
2112
|
${name}
|
|
2122
2113
|
Usage: ${name} [options]
|
|
2123
2114
|
Options:
|
|
2124
|
-
--http
|
|
2125
|
-
--stdio
|
|
2126
|
-
--host <host>
|
|
2127
|
-
--port <port>
|
|
2128
|
-
--
|
|
2115
|
+
--http Use HTTP streaming transport (requires HOSTINGER_API_TOKEN env var)
|
|
2116
|
+
--stdio Use standard input/output transport (default)
|
|
2117
|
+
--host <host> Host to bind to (default: 127.0.0.1)
|
|
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
|
|
2121
|
+
--help Show this help message
|
|
2129
2122
|
Environment Variables:
|
|
2130
|
-
|
|
2131
|
-
|
|
2123
|
+
HOSTINGER_API_TOKEN Hostinger API token (overrides OAuth when set)
|
|
2124
|
+
API_TOKEN Deprecated alias for HOSTINGER_API_TOKEN (will be removed in a future version)
|
|
2125
|
+
OAUTH_ISSUER OAuth server base URL (default: https://auth.hostinger.com)
|
|
2126
|
+
DEBUG Enable debug logging (true/false)
|
|
2132
2127
|
`);
|
|
2133
2128
|
process.exit(0);
|
|
2134
2129
|
}
|
|
2130
|
+
|
|
2131
|
+
if (argv.login) {
|
|
2132
|
+
const provider = new OAuthProvider();
|
|
2133
|
+
console.error('[OAuth] Starting sign-in flow...');
|
|
2134
|
+
await provider.login();
|
|
2135
|
+
console.error('[OAuth] Sign-in successful. Credentials stored.');
|
|
2136
|
+
process.exit(0);
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
if (argv.logout) {
|
|
2140
|
+
const provider = new OAuthProvider();
|
|
2141
|
+
await provider.logout();
|
|
2142
|
+
console.error('[OAuth] Signed out. Stored credentials revoked and cleared.');
|
|
2143
|
+
process.exit(0);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
if (argv.http) {
|
|
2147
|
+
const envToken = getEnvToken();
|
|
2148
|
+
if (!envToken) {
|
|
2149
|
+
console.error('[Error] HTTP transport requires the HOSTINGER_API_TOKEN environment variable. OAuth sign-in is only supported in stdio mode.');
|
|
2150
|
+
process.exit(1);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2135
2154
|
const server = new MCPServer({ name, version, tools });
|
|
2136
2155
|
if (argv.http) {
|
|
2137
2156
|
await server.startHttp(argv.host, argv.port);
|
package/src/core/tools/all.js
CHANGED
|
@@ -1570,6 +1570,10 @@ export default [
|
|
|
1570
1570
|
"type": "string",
|
|
1571
1571
|
"description": "surname parameter"
|
|
1572
1572
|
},
|
|
1573
|
+
"phone": {
|
|
1574
|
+
"type": "string",
|
|
1575
|
+
"description": "Phone number in E.164 format (leading \"+\" then 7-15 digits)"
|
|
1576
|
+
},
|
|
1573
1577
|
"note": {
|
|
1574
1578
|
"type": "string",
|
|
1575
1579
|
"description": "note parameter"
|
|
@@ -1787,6 +1791,10 @@ export default [
|
|
|
1787
1791
|
"type": "string",
|
|
1788
1792
|
"description": "surname parameter"
|
|
1789
1793
|
},
|
|
1794
|
+
"phone": {
|
|
1795
|
+
"type": "string",
|
|
1796
|
+
"description": "Phone number in E.164 format (leading \"+\" then 7-15 digits)"
|
|
1797
|
+
},
|
|
1790
1798
|
"note": {
|
|
1791
1799
|
"type": "string",
|
|
1792
1800
|
"description": "note parameter"
|
package/src/core/tools/all.ts
CHANGED
|
@@ -1580,6 +1580,10 @@ const tools: OpenApiTool[] = [
|
|
|
1580
1580
|
"type": "string",
|
|
1581
1581
|
"description": "surname parameter"
|
|
1582
1582
|
},
|
|
1583
|
+
"phone": {
|
|
1584
|
+
"type": "string",
|
|
1585
|
+
"description": "Phone number in E.164 format (leading \"+\" then 7-15 digits)"
|
|
1586
|
+
},
|
|
1583
1587
|
"note": {
|
|
1584
1588
|
"type": "string",
|
|
1585
1589
|
"description": "note parameter"
|
|
@@ -1797,6 +1801,10 @@ const tools: OpenApiTool[] = [
|
|
|
1797
1801
|
"type": "string",
|
|
1798
1802
|
"description": "surname parameter"
|
|
1799
1803
|
},
|
|
1804
|
+
"phone": {
|
|
1805
|
+
"type": "string",
|
|
1806
|
+
"description": "Phone number in E.164 format (leading \"+\" then 7-15 digits)"
|
|
1807
|
+
},
|
|
1800
1808
|
"note": {
|
|
1801
1809
|
"type": "string",
|
|
1802
1810
|
"description": "note parameter"
|
package/src/core/tools/reach.js
CHANGED
|
@@ -95,6 +95,10 @@ export default [
|
|
|
95
95
|
"type": "string",
|
|
96
96
|
"description": "surname parameter"
|
|
97
97
|
},
|
|
98
|
+
"phone": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"description": "Phone number in E.164 format (leading \"+\" then 7-15 digits)"
|
|
101
|
+
},
|
|
98
102
|
"note": {
|
|
99
103
|
"type": "string",
|
|
100
104
|
"description": "note parameter"
|
|
@@ -312,6 +316,10 @@ export default [
|
|
|
312
316
|
"type": "string",
|
|
313
317
|
"description": "surname parameter"
|
|
314
318
|
},
|
|
319
|
+
"phone": {
|
|
320
|
+
"type": "string",
|
|
321
|
+
"description": "Phone number in E.164 format (leading \"+\" then 7-15 digits)"
|
|
322
|
+
},
|
|
315
323
|
"note": {
|
|
316
324
|
"type": "string",
|
|
317
325
|
"description": "note parameter"
|
package/src/core/tools/reach.ts
CHANGED
|
@@ -105,6 +105,10 @@ const tools: OpenApiTool[] = [
|
|
|
105
105
|
"type": "string",
|
|
106
106
|
"description": "surname parameter"
|
|
107
107
|
},
|
|
108
|
+
"phone": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"description": "Phone number in E.164 format (leading \"+\" then 7-15 digits)"
|
|
111
|
+
},
|
|
108
112
|
"note": {
|
|
109
113
|
"type": "string",
|
|
110
114
|
"description": "note parameter"
|
|
@@ -322,6 +326,10 @@ const tools: OpenApiTool[] = [
|
|
|
322
326
|
"type": "string",
|
|
323
327
|
"description": "surname parameter"
|
|
324
328
|
},
|
|
329
|
+
"phone": {
|
|
330
|
+
"type": "string",
|
|
331
|
+
"description": "Phone number in E.164 format (leading \"+\" then 7-15 digits)"
|
|
332
|
+
},
|
|
325
333
|
"note": {
|
|
326
334
|
"type": "string",
|
|
327
335
|
"description": "note parameter"
|
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
package/types.d.ts
CHANGED
|
@@ -1019,6 +1019,10 @@ the contact will be created with a pending status and a confirmation email will
|
|
|
1019
1019
|
* surname parameter
|
|
1020
1020
|
*/
|
|
1021
1021
|
surname?: string;
|
|
1022
|
+
/**
|
|
1023
|
+
* Phone number in E.164 format (leading "+" then 7-15 digits)
|
|
1024
|
+
*/
|
|
1025
|
+
phone?: string;
|
|
1022
1026
|
/**
|
|
1023
1027
|
* note parameter
|
|
1024
1028
|
*/
|
|
@@ -1129,6 +1133,10 @@ and a confirmation email will be sent.
|
|
|
1129
1133
|
* surname parameter
|
|
1130
1134
|
*/
|
|
1131
1135
|
surname?: string;
|
|
1136
|
+
/**
|
|
1137
|
+
* Phone number in E.164 format (leading "+" then 7-15 digits)
|
|
1138
|
+
*/
|
|
1139
|
+
phone?: string;
|
|
1132
1140
|
/**
|
|
1133
1141
|
* note parameter
|
|
1134
1142
|
*/
|