hostinger-api-mcp 1.61.1 → 1.62.0

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/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ Generated at release time from the diff between the previous and current
4
4
  build. Tool changes are listed individually; everything else is summarised
5
5
  by area.
6
6
 
7
+ ## v1.62.0 — 2026-09-21
8
+
9
+ ### Also in this release
10
+
11
+ - Bundled agent skills: `skills/headless/SKILL.md`, `skills/headless/references/SETUP.md`, `skills/headless/references/DATABASE.md`
12
+ - Request handling and authentication: `src/core/runtime.js`, `src/core/runtime.ts`
13
+ - Server entry points: 6 files
14
+ - Server instructions: `src/core/instructions.js`, `src/core/instructions.ts`
15
+
16
+ ## v1.61.2 — 2026-09-21
17
+
18
+ ### Also in this release
19
+
20
+ - Dependencies: `package-lock.json`
21
+ - Request handling and authentication: `src/core/runtime.js`, `src/core/runtime.ts`
22
+ - Other files: `.gitignore`
23
+
7
24
  ## v1.61.1 — 2026-09-17
8
25
 
9
26
  ### Also in this release
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hostinger-api-mcp",
3
- "version": "1.61.1",
3
+ "version": "1.62.0",
4
4
  "description": "Hostinger API — manage domains, DNS, hosting, VPS, billing and more from Gemini CLI.",
5
5
  "mcpServers": {
6
6
  "hostinger": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hostinger-api-mcp",
3
- "version": "1.61.1",
3
+ "version": "1.62.0",
4
4
  "mcpName": "io.github.hostinger/hostinger-api-mcp",
5
5
  "description": "MCP server for Hostinger API",
6
6
  "repository": {
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/hostinger/api-mcp-server",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.61.1",
9
+ "version": "1.62.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@hostinger/mcp",
14
- "version": "1.61.1",
14
+ "version": "1.62.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -31,7 +31,7 @@ If signals conflict, ask the user — don't guess.
31
31
  2. **Setup** (`references/SETUP.md`) — plan check, then provision the website on a free subdomain (or the user's own domain) and wait until it's ready.
32
32
  3. **Store** (`references/STORE.md`, only when commerce is needed) — resolve or create the store and its `custom` sales channel, seed products/shipping/payment, and note the `sales_channel_id` for the frontend.
33
33
  4. **Content backend** (`references/WORDPRESS.md`, only when owner-managed content is needed) — install WordPress on a dedicated subdomain, wait until it's ready, and hand the owner a wp-admin login link. The frontend reads it through the public WP REST API.
34
- 5. **Build** — create or wire the frontend. For store runs, follow the frontend contract in `references/STORE.md` (catalog fetched at runtime from the public Storefront API, cart in `localStorage`, client-side checkout — never embed an API token in the site). For content runs, follow the frontend contract in `references/WORDPRESS.md` (posts fetched at runtime from the WP REST API, rendered HTML bodies, graceful empty states).
34
+ 5. **Build** — create or wire the frontend. For store runs, follow the frontend contract in `references/STORE.md` (catalog fetched at runtime from the public Storefront API, cart in `localStorage`, client-side checkout — never embed an API token in the site). For content runs, follow the frontend contract in `references/WORDPRESS.md` (posts fetched at runtime from the WP REST API, rendered HTML bodies, graceful empty states). When the app needs MySQL, follow `references/DATABASE.md` (which host each runtime connects to, credentials via env vars or a server-side config file, never in the frontend).
35
35
  6. **Deploy** (`references/DEPLOYMENT.md`) — archive and deploy via the matching hosting tool, polling build logs for Node.js runs.
36
36
  7. **Verify** — curl the live URL for a 200 and a piece of real page copy; for store runs, confirm a checkout POST returns a redirect URL; for content runs, confirm the WP REST API returns 200 and the frontend renders posts. Show the user the live URL and where to manage things (hPanel: https://hpanel.hostinger.com, the store dashboard for commerce, wp-admin for content).
37
37
  8. **Record** — write `.hostinger/site.json` into the project: `{ "domain", "username", "type": "static"|"nodejs", "sales_channel_id"?, "store_id"?, "cms_domain"? }`. This is what makes future runs resolve as `iterate`.
@@ -46,6 +46,7 @@ Run non-interactively wherever possible. The exceptions that must involve the us
46
46
  | Store: seed the backend + the frontend API contract | `references/STORE.md` |
47
47
  | WordPress: headless CMS/blog backend + the frontend read contract | `references/WORDPRESS.md` |
48
48
  | Deploy: static vs Node.js, archive rules, logs, verify | `references/DEPLOYMENT.md` |
49
+ | Database + runtime: MySQL host per runtime, env vars, what to poll, runtime logs | `references/DATABASE.md` |
49
50
 
50
51
  ## Where the how comes from
51
52
 
@@ -0,0 +1,49 @@
1
+ # Database and runtime — MySQL hosts, env vars, and what to poll
2
+
3
+ Use this when the app needs MySQL, or when a deploy "succeeded" but the app cannot reach its database. Everything here uses the **hosting** MCP tools; Agency Plan differences are at the end.
4
+
5
+ ## 1. Create the database
6
+
7
+ 1. `hosting_createAccountDatabaseV1` on the site's `username` with a database name, a user and a password you generate — name and user are prefixed with the account username automatically.
8
+ 2. Read the full, prefixed `name` and `user` back from `hosting_listAccountDatabasesV1`; every other database tool wants the full name.
9
+ 3. Do not print the password to the user once it is stored, and never commit it.
10
+
11
+ ## 2. Which host the app connects to
12
+
13
+ | Where the code runs | Host | Port | Why |
14
+ | --- | --- | --- | --- |
15
+ | PHP on the website | `localhost` or `127.0.0.1` | 3306 | `localhost` uses the MySQL socket; both are granted |
16
+ | Node.js on the website | `127.0.0.1` | 3306 | `localhost` may resolve to IPv6 `::1`, and not every database user has that grant |
17
+ | Outside Hostinger (a laptop, another server) | the `host` from `hosting_listAccountDatabasesV1` (`srvNNNN.hstgr.io`) | 3306 | needs `hosting_createDatabaseRemoteConnectionV1` for that client's IP first — never `%`, it opens the database to every address on the internet |
18
+
19
+ Do not put the remote `host` into the deployed app — it has no grant for connections that come from the server itself.
20
+
21
+ ## 3. Hand credentials to the app
22
+
23
+ - **Node.js:** environment variables via `hosting_replaceNode_jsEnvironmentVariablesV1`. Conventional keys: `DB_HOST=127.0.0.1`, `DB_PORT=3306`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`, plus `DATABASE_URL=mysql://USER:PASSWORD@127.0.0.1:3306/NAME` for ORMs that want one string — URL-encode `USER` and `PASSWORD` there (`encodeURIComponent`), or a password containing `@`, `:`, `/` or `#` breaks the URL. The call is a **full replace**: list the current keys with `hosting_listNode_jsEnvironmentVariablesV1` (values come back masked as `********` — never copy those), then send the complete set with real values. Saving restarts the process; frameworks that bake variables into the build (Next.js, `NEXT_PUBLIC_*`) need a new `hosting_startNode_jsBuildV1` afterwards.
24
+ - **PHP:** a config file on the server that the app reads (its own `config.php`, WordPress's `wp-config.php`). Ship it with the deploy, keep it out of git.
25
+ - **Never** in client-side JavaScript, in a committed `.env`, or in chat replies.
26
+
27
+ ## 4. What is asynchronous, and what to poll
28
+
29
+ | You called | Poll this until | Typical wait |
30
+ | --- | --- | --- |
31
+ | `hosting_createWebsiteV1` | `hosting_listWebsitesV1` lists the domain | up to a few minutes |
32
+ | `hosting_deployJsApplication` | `hosting_listJsDeployments` shows the build finished | minutes |
33
+ | `hosting_startNode_jsBuildV1` | `hosting_getNode_jsBuildDetailsV1` state is `completed` or `failed` | minutes |
34
+ | `hosting_installWordPressV1`, plugin/theme/core jobs | `hosting_listWordPressInstallationsV1` lists the install | 1–2 minutes |
35
+ | `hosting_repairDatabaseV1`, `hosting_deleteWebsiteV1` | nothing to poll — runs in the background | minutes |
36
+ | `agency-hosting_createANewWebsiteV1` | `agency-hosting_getWebsiteSetupStatusV1` with `setup_uuid` is `completed` | minutes |
37
+
38
+ Poll with backoff (a few seconds, then tens of seconds). A queued response is not a failure, and re-sending the write does not speed it up.
39
+
40
+ ## 5. When the app is up but broken
41
+
42
+ - Build failed: `hosting_getNodeJSBuildLogsV1` for the raw log, `hosting_analyseFailedNode_jsBuildV1` for a diagnosis.
43
+ - Build passed, app errors: `hosting_getNode_jsRuntimeLogsV1` with a `period` such as `1h`; entries before `last_deployed_at` belong to the previous deploy.
44
+ - `Access denied for user 'x'@'::1'`: the app used `localhost` from Node.js — set `DB_HOST` to `127.0.0.1`.
45
+ - PHP limits (memory, upload size): `hosting_getPHPDetailsV1`, then `hosting_updatePHPOptionsV1`.
46
+
47
+ ## Agency Plan websites (`agency-hosting_*`)
48
+
49
+ `agency-hosting_createWebsiteDatabaseV1` creates the database and its single user; the user is granted on `localhost`, so the PHP app connects to `localhost`. Deploys are synchronous there — `agency-hosting_deployPhpApplication` returns when the site is live. Other jobs (SSL, backups) show up in `agency-hosting_listWebsiteProcessesV1`.
@@ -30,5 +30,5 @@ If the domain already has a website (an `iterate` run, or the user pointed at an
30
30
 
31
31
  ## 4. Optional extras (only when the run needs them)
32
32
 
33
- - **Database:** `hosting_createAccountDatabaseV1` for Node.js apps that need MySQL; pass the credentials to the app via its environment/config, never hard-coded into client-side code.
33
+ - **Database:** when the app needs MySQL, follow `DATABASE.md` — creating it, which host each runtime connects to (Node.js must use `127.0.0.1`), and handing the credentials to the app without hard-coding them.
34
34
  - **DNS records:** the DNS tools (`DNS_updateDNSRecordsV1` etc.) for custom-domain records; take a snapshot (`DNS_getDNSSnapshotListV1` context) before destructive changes.
@@ -0,0 +1,2 @@
1
+ // Auto-generated server instructions
2
+ export default "Hostinger API notes for agents. Tool descriptions are authoritative for request shapes; read them before calling.\n\nDatabases (web hosting, hosting_* tools)\n- MySQL runs on the same server as the website. From the deployed app connect to host 127.0.0.1, port 3306. PHP may also use localhost (Unix socket). Node.js should use 127.0.0.1: localhost can resolve to IPv6 (::1) and not every database user has that grant.\n- The `host` from hosting_listAccountDatabasesV1 (srvNNNN.hstgr.io) is only for connections from outside Hostinger, after hosting_createDatabaseRemoteConnectionV1. Never use it from the app.\n- Agency Plan (agency-hosting_* tools) database users are created for localhost; the PHP app connects there.\n\nAsynchronous operations\n- A success response often means \"queued\", not \"done\". Poll the matching read tool with backoff (seconds, then minutes). Never re-send the write.\n- hosting_createWebsiteV1 -> poll hosting_listWebsitesV1 until the domain appears (a few minutes).\n- hosting_startNode_jsBuildV1 -> hosting_getNode_jsBuildDetailsV1; logs via hosting_getNodeJSBuildLogsV1; on failure hosting_analyseFailedNode_jsBuildV1.\n- hosting_installWordPressV1 and WordPress plugin/theme/core jobs -> poll hosting_listWordPressInstallationsV1.\n- agency-hosting_createANewWebsiteV1 -> poll agency-hosting_getWebsiteSetupStatusV1 with setup_uuid; other Agency Plan jobs -> agency-hosting_listWebsiteProcessesV1.\n- Website deletion, database repair and domain contact changes finish in the background.\n\nCredentials\n- Never write database passwords or API tokens into source code, client-side JavaScript or committed .env files.\n- Node.js apps read them from environment variables: set the full set with hosting_replaceNode_jsEnvironmentVariablesV1 (full replace; values are masked on read). Build-time variables (Next.js) need a new build afterwards.\n- PHP apps read them from a config file on the server, outside the repository.\n- Generate strong passwords and do not echo them back once stored.\n\nFor end-to-end site builds install the headless skill: npx skills add hostinger/api-mcp-server";
@@ -0,0 +1,3 @@
1
+ // Auto-generated server instructions
2
+ const instructions: string = "Hostinger API notes for agents. Tool descriptions are authoritative for request shapes; read them before calling.\n\nDatabases (web hosting, hosting_* tools)\n- MySQL runs on the same server as the website. From the deployed app connect to host 127.0.0.1, port 3306. PHP may also use localhost (Unix socket). Node.js should use 127.0.0.1: localhost can resolve to IPv6 (::1) and not every database user has that grant.\n- The `host` from hosting_listAccountDatabasesV1 (srvNNNN.hstgr.io) is only for connections from outside Hostinger, after hosting_createDatabaseRemoteConnectionV1. Never use it from the app.\n- Agency Plan (agency-hosting_* tools) database users are created for localhost; the PHP app connects there.\n\nAsynchronous operations\n- A success response often means \"queued\", not \"done\". Poll the matching read tool with backoff (seconds, then minutes). Never re-send the write.\n- hosting_createWebsiteV1 -> poll hosting_listWebsitesV1 until the domain appears (a few minutes).\n- hosting_startNode_jsBuildV1 -> hosting_getNode_jsBuildDetailsV1; logs via hosting_getNodeJSBuildLogsV1; on failure hosting_analyseFailedNode_jsBuildV1.\n- hosting_installWordPressV1 and WordPress plugin/theme/core jobs -> poll hosting_listWordPressInstallationsV1.\n- agency-hosting_createANewWebsiteV1 -> poll agency-hosting_getWebsiteSetupStatusV1 with setup_uuid; other Agency Plan jobs -> agency-hosting_listWebsiteProcessesV1.\n- Website deletion, database repair and domain contact changes finish in the background.\n\nCredentials\n- Never write database passwords or API tokens into source code, client-side JavaScript or committed .env files.\n- Node.js apps read them from environment variables: set the full set with hosting_replaceNode_jsEnvironmentVariablesV1 (full replace; values are masked on read). Build-time variables (Next.js) need a new build afterwards.\n- PHP apps read them from a config file on the server, outside the repository.\n- Generate strong passwords and do not echo them back once stored.\n\nFor end-to-end site builds install the headless skill: npx skills add hostinger/api-mcp-server";
3
+ export default instructions;
@@ -10,6 +10,8 @@ import { config as dotenvConfig } from "dotenv";
10
10
  import {
11
11
  ListToolsRequestSchema,
12
12
  CallToolRequestSchema,
13
+ ErrorCode,
14
+ McpError,
13
15
  } from "@modelcontextprotocol/sdk/types.js";
14
16
  import { OAuthProvider, getEnvToken } from "./oauth.js";
15
17
  import * as tus from "tus-js-client";
@@ -19,6 +21,19 @@ import path from "path";
19
21
  // Load environment variables
20
22
  dotenvConfig({ quiet: true });
21
23
 
24
+ /**
25
+ * An upstream API call that came back as a failure. Carries the status and the
26
+ * API's own response body so the CallTool handler can hand both to the model.
27
+ */
28
+ class ApiExecutionError extends Error {
29
+ constructor(status, data, message) {
30
+ super(message || `API request failed with status ${status}`);
31
+ this.name = 'ApiExecutionError';
32
+ this.status = status;
33
+ this.responseData = data;
34
+ }
35
+ }
36
+
22
37
  const SECURITY_SCHEMES = {
23
38
  "apiToken": {
24
39
  "type": "http",
@@ -32,11 +47,12 @@ const SECURITY_SCHEMES = {
32
47
  * Generated from OpenAPI spec version 1.53.1
33
48
  */
34
49
  class MCPServer {
35
- constructor({ name, version, tools }) {
50
+ constructor({ name, version, tools, instructions }) {
36
51
  // Initialize class properties
37
52
  this.name = name;
38
53
  this.version = version;
39
54
  this.toolList = tools;
55
+ this.instructions = instructions;
40
56
  this.server = null;
41
57
  this.tools = new Map();
42
58
  this.debug = process.env.DEBUG === "true";
@@ -57,6 +73,7 @@ class MCPServer {
57
73
  capabilities: {
58
74
  tools: {}, // Enable tools capability
59
75
  },
76
+ instructions: this.instructions,
60
77
  }
61
78
  );
62
79
 
@@ -157,13 +174,17 @@ class MCPServer {
157
174
  }
158
175
  }
159
176
 
177
+ // An unknown tool is a protocol error, not a tool execution error: the
178
+ // model cannot self-correct its way out of a name the server does not
179
+ // have. -32602 is what the spec's example and the SDK's own McpServer
180
+ // use; a plain Error would go out as -32603 instead.
160
181
  if (!toolName) {
161
- throw new Error(`Tool not found: ${name}`);
182
+ throw new McpError(ErrorCode.InvalidParams, `Tool not found: ${name}`);
162
183
  }
163
184
 
164
185
  toolDetails = this.toolList.find(t => t.name === toolName);
165
186
  if (!toolDetails) {
166
- throw new Error(`Tool details not found for ID: ${toolName}`);
187
+ throw new McpError(ErrorCode.InternalError, `Tool details not found for ID: ${toolName}`);
167
188
  }
168
189
 
169
190
  try {
@@ -189,10 +210,32 @@ class MCPServer {
189
210
 
190
211
  } catch (error) {
191
212
  const errorMessage = error instanceof Error ? error.message : String(error);
192
- const response = error.response;
193
213
  this.log('error', `Error executing tool ${name}: ${errorMessage}`);
194
214
 
195
- throw error;
215
+ // MCP reports tool execution failures (API errors, bad input, business
216
+ // logic) inside the result with isError: true. Throwing here would turn
217
+ // them into JSON-RPC protocol errors instead, which the spec reserves
218
+ // for unknown tools and malformed requests — those are still thrown,
219
+ // above, before this try. The status stays in the text so the model can
220
+ // tell a retryable 429/503 from a 403 it should not retry.
221
+ let errorText = errorMessage;
222
+ if (error instanceof ApiExecutionError) {
223
+ const payload = error.responseData;
224
+ const body = payload === null || payload === undefined || payload === ''
225
+ ? errorMessage
226
+ : typeof payload === 'object' ? JSON.stringify(payload) : String(payload);
227
+ errorText = error.status ? `HTTP ${error.status}: ${body}` : body;
228
+ }
229
+
230
+ return {
231
+ isError: true,
232
+ content: [
233
+ {
234
+ type: "text",
235
+ text: errorText
236
+ }
237
+ ]
238
+ };
196
239
  }
197
240
  });
198
241
  }
@@ -2474,6 +2517,13 @@ class MCPServer {
2474
2517
  this.log('debug', `Retry response status: ${response.status}`);
2475
2518
  }
2476
2519
 
2520
+ // validateStatus resolves everything under 500, so a 4xx arrives here as
2521
+ // an ordinary response. Without this it would be returned to the model as
2522
+ // a successful tool result whose body happens to be the API's error.
2523
+ if (response.status >= 400) {
2524
+ throw new ApiExecutionError(response.status, response.data);
2525
+ }
2526
+
2477
2527
  return response.data;
2478
2528
 
2479
2529
  } catch (error) {
@@ -2489,10 +2539,14 @@ class MCPServer {
2489
2539
  data: typeof responseData === 'object' ? JSON.stringify(responseData) : responseData
2490
2540
  });
2491
2541
 
2492
- // Rethrow with more context for better error handling
2493
- const detailedError = new Error(`API request failed with status ${responseStatus}: ${errorMessage}`);
2494
- detailedError.response = error.response;
2495
- throw detailedError;
2542
+ // Structured so the CallTool handler can report this as an MCP tool
2543
+ // execution error carrying the API's own payload. Status is 0 for a
2544
+ // transport failure (DNS, ECONNREFUSED, timeout) — there is no response.
2545
+ throw new ApiExecutionError(
2546
+ responseStatus ?? 0,
2547
+ responseData,
2548
+ `API request failed with status ${responseStatus}: ${errorMessage}`
2549
+ );
2496
2550
  }
2497
2551
 
2498
2552
  throw error;
@@ -2606,7 +2660,7 @@ class MCPServer {
2606
2660
  }
2607
2661
  }
2608
2662
 
2609
- export async function startServer({ name, version, tools }) {
2663
+ export async function startServer({ name, version, tools, instructions }) {
2610
2664
  const argv = minimist(process.argv.slice(2), {
2611
2665
  string: ['host'],
2612
2666
  boolean: ['stdio', 'http', 'help', 'version', 'login', 'logout'],
@@ -2664,7 +2718,7 @@ export async function startServer({ name, version, tools }) {
2664
2718
  }
2665
2719
  }
2666
2720
 
2667
- const server = new MCPServer({ name, version, tools });
2721
+ const server = new MCPServer({ name, version, tools, instructions });
2668
2722
  if (argv.http) {
2669
2723
  await server.startHttp(argv.host, argv.port);
2670
2724
  } else {
@@ -10,6 +10,8 @@ import { config as dotenvConfig } from "dotenv";
10
10
  import {
11
11
  ListToolsRequestSchema,
12
12
  CallToolRequestSchema,
13
+ ErrorCode,
14
+ McpError,
13
15
  Tool,
14
16
  } from "@modelcontextprotocol/sdk/types.js";
15
17
  import { OAuthProvider, getEnvToken } from "./oauth.js";
@@ -42,6 +44,22 @@ interface SecurityScheme {
42
44
  scheme?: string;
43
45
  }
44
46
 
47
+ /**
48
+ * An upstream API call that came back as a failure. Carries the status and the
49
+ * API's own response body so the CallTool handler can hand both to the model.
50
+ */
51
+ class ApiExecutionError extends Error {
52
+ public readonly status: number;
53
+ public readonly responseData: any;
54
+
55
+ constructor(status: number, data: any, message?: string) {
56
+ super(message || `API request failed with status ${status}`);
57
+ this.name = 'ApiExecutionError';
58
+ this.status = status;
59
+ this.responseData = data;
60
+ }
61
+ }
62
+
45
63
  const SECURITY_SCHEMES: Record<string, SecurityScheme> = {
46
64
  "apiToken": {
47
65
  "type": "http",
@@ -60,16 +78,18 @@ class MCPServer {
60
78
  private readonly toolList: OpenApiTool[];
61
79
  private server: Server;
62
80
  private tools: Map<string, Tool> = new Map();
81
+ private instructions?: string;
63
82
  private debug: boolean;
64
83
  private baseUrl: string;
65
84
  private headers: Record<string, string>;
66
85
  private oauth: OAuthProvider;
67
86
 
68
- constructor({ name, version, tools }: { name: string; version: string; tools: OpenApiTool[] }) {
87
+ constructor({ name, version, tools, instructions }: { name: string; version: string; tools: OpenApiTool[]; instructions?: string }) {
69
88
  // Initialize class properties
70
89
  this.name = name;
71
90
  this.version = version;
72
91
  this.toolList = tools;
92
+ this.instructions = instructions;
73
93
  this.debug = process.env.DEBUG === "true";
74
94
  this.baseUrl = process.env.API_BASE_URL || "https://developers.hostinger.com";
75
95
  this.headers = this.parseHeaders(process.env.API_HEADERS || "");
@@ -88,6 +108,7 @@ class MCPServer {
88
108
  capabilities: {
89
109
  tools: {}, // Enable tools capability
90
110
  },
111
+ instructions: this.instructions,
91
112
  }
92
113
  );
93
114
 
@@ -188,13 +209,17 @@ class MCPServer {
188
209
  }
189
210
  }
190
211
 
212
+ // An unknown tool is a protocol error, not a tool execution error: the
213
+ // model cannot self-correct its way out of a name the server does not
214
+ // have. -32602 is what the spec's example and the SDK's own McpServer
215
+ // use; a plain Error would go out as -32603 instead.
191
216
  if (!toolName) {
192
- throw new Error(`Tool not found: ${name}`)
217
+ throw new McpError(ErrorCode.InvalidParams, `Tool not found: ${name}`);
193
218
  }
194
219
 
195
220
  toolDetails = this.toolList.find(t => t.name === toolName);
196
221
  if (!toolDetails) {
197
- throw new Error(`Tool details not found for ID: ${toolName}`);
222
+ throw new McpError(ErrorCode.InternalError, `Tool details not found for ID: ${toolName}`);
198
223
  }
199
224
 
200
225
  try {
@@ -221,8 +246,31 @@ class MCPServer {
221
246
  } catch (error) {
222
247
  const errorMessage = error instanceof Error ? error.message : String(error);
223
248
  this.log('error', `Error executing tool ${toolName}: ${errorMessage}`);
224
-
225
- throw error;
249
+
250
+ // MCP reports tool execution failures (API errors, bad input, business
251
+ // logic) inside the result with isError: true. Throwing here would turn
252
+ // them into JSON-RPC protocol errors instead, which the spec reserves
253
+ // for unknown tools and malformed requests — those are still thrown,
254
+ // above, before this try. The status stays in the text so the model can
255
+ // tell a retryable 429/503 from a 403 it should not retry.
256
+ let errorText: string = errorMessage;
257
+ if (error instanceof ApiExecutionError) {
258
+ const payload = error.responseData;
259
+ const body = payload === null || payload === undefined || payload === ''
260
+ ? errorMessage
261
+ : typeof payload === 'object' ? JSON.stringify(payload) : String(payload);
262
+ errorText = error.status ? `HTTP ${error.status}: ${body}` : body;
263
+ }
264
+
265
+ return {
266
+ isError: true,
267
+ content: [
268
+ {
269
+ type: "text",
270
+ text: errorText
271
+ }
272
+ ]
273
+ };
226
274
  }
227
275
  });
228
276
  }
@@ -2524,6 +2572,13 @@ class MCPServer {
2524
2572
  this.log('debug', `Retry response status: ${response.status}`);
2525
2573
  }
2526
2574
 
2575
+ // validateStatus resolves everything under 500, so a 4xx arrives here as
2576
+ // an ordinary response. Without this it would be returned to the model as
2577
+ // a successful tool result whose body happens to be the API's error.
2578
+ if (response.status >= 400) {
2579
+ throw new ApiExecutionError(response.status, response.data);
2580
+ }
2581
+
2527
2582
  return response.data;
2528
2583
 
2529
2584
  } catch (error) {
@@ -2540,10 +2595,14 @@ class MCPServer {
2540
2595
  data: typeof responseData === 'object' ? JSON.stringify(responseData) : String(responseData)
2541
2596
  });
2542
2597
 
2543
- // Rethrow with more context for better error handling
2544
- const detailedError = new Error(`API request failed with status ${responseStatus}: ${errorMessage}`);
2545
- (detailedError as any).response = axiosError.response;
2546
- throw detailedError;
2598
+ // Structured so the CallTool handler can report this as an MCP tool
2599
+ // execution error carrying the API's own payload. Status is 0 for a
2600
+ // transport failure (DNS, ECONNREFUSED, timeout) — there is no response.
2601
+ throw new ApiExecutionError(
2602
+ responseStatus ?? 0,
2603
+ responseData,
2604
+ `API request failed with status ${responseStatus}: ${errorMessage}`
2605
+ );
2547
2606
  }
2548
2607
 
2549
2608
  throw error;
@@ -2657,7 +2716,7 @@ class MCPServer {
2657
2716
  }
2658
2717
  }
2659
2718
 
2660
- export async function startServer({ name, version, tools }: { name: string; version: string; tools: OpenApiTool[] }): Promise<void> {
2719
+ export async function startServer({ name, version, tools, instructions }: { name: string; version: string; tools: OpenApiTool[]; instructions?: string }): Promise<void> {
2661
2720
  const argv = minimist(process.argv.slice(2), {
2662
2721
  string: ['host'],
2663
2722
  boolean: ['stdio', 'http', 'help', 'version', 'login', 'logout'],
@@ -2715,7 +2774,7 @@ export async function startServer({ name, version, tools }: { name: string; vers
2715
2774
  }
2716
2775
  }
2717
2776
 
2718
- const server = new MCPServer({ name, version, tools });
2777
+ const server = new MCPServer({ name, version, tools, instructions });
2719
2778
  if (argv.http) {
2720
2779
  await server.startHttp(argv.host, argv.port);
2721
2780
  } else {
@@ -2,5 +2,6 @@
2
2
  // Auto-generated entry for group: agency-hosting
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/agency-hosting.js';
5
+ import instructions from '../core/instructions.js';
5
6
 
6
- startServer({ name: 'hostinger-agency-hosting-mcp', version: '1.61.1', tools });
7
+ startServer({ name: 'hostinger-agency-hosting-mcp', version: '1.62.0', tools, instructions });
@@ -2,5 +2,6 @@
2
2
  // Auto-generated entry for group: agency-hosting
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/agency-hosting.js';
5
+ import instructions from '../core/instructions.js';
5
6
 
6
- startServer({ name: 'hostinger-agency-hosting-mcp', version: '1.61.1', tools });
7
+ startServer({ name: 'hostinger-agency-hosting-mcp', version: '1.62.0', tools, instructions });
@@ -2,5 +2,6 @@
2
2
  // Auto-generated entry for group: all
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/all.js';
5
+ import instructions from '../core/instructions.js';
5
6
 
6
- startServer({ name: 'hostinger-api-mcp', version: '1.61.1', tools });
7
+ startServer({ name: 'hostinger-api-mcp', version: '1.62.0', tools, instructions });
@@ -2,5 +2,6 @@
2
2
  // Auto-generated entry for group: all
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/all.js';
5
+ import instructions from '../core/instructions.js';
5
6
 
6
- startServer({ name: 'hostinger-api-mcp', version: '1.61.1', tools });
7
+ startServer({ name: 'hostinger-api-mcp', version: '1.62.0', tools, instructions });
@@ -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: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-billing-mcp', version: '1.62.0', 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: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-billing-mcp', version: '1.62.0', 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: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-dns-mcp', version: '1.62.0', 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: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-dns-mcp', version: '1.62.0', 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: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-domains-mcp', version: '1.62.0', 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: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-domains-mcp', version: '1.62.0', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/ecommerce.js';
5
5
 
6
- startServer({ name: 'hostinger-ecommerce-mcp', version: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-ecommerce-mcp', version: '1.62.0', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/ecommerce.js';
5
5
 
6
- startServer({ name: 'hostinger-ecommerce-mcp', version: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-ecommerce-mcp', version: '1.62.0', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/horizons.js';
5
5
 
6
- startServer({ name: 'hostinger-horizons-mcp', version: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-horizons-mcp', version: '1.62.0', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/horizons.js';
5
5
 
6
- startServer({ name: 'hostinger-horizons-mcp', version: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-horizons-mcp', version: '1.62.0', tools });
@@ -2,5 +2,6 @@
2
2
  // Auto-generated entry for group: hosting
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/hosting.js';
5
+ import instructions from '../core/instructions.js';
5
6
 
6
- startServer({ name: 'hostinger-hosting-mcp', version: '1.61.1', tools });
7
+ startServer({ name: 'hostinger-hosting-mcp', version: '1.62.0', tools, instructions });
@@ -2,5 +2,6 @@
2
2
  // Auto-generated entry for group: hosting
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/hosting.js';
5
+ import instructions from '../core/instructions.js';
5
6
 
6
- startServer({ name: 'hostinger-hosting-mcp', version: '1.61.1', tools });
7
+ startServer({ name: 'hostinger-hosting-mcp', version: '1.62.0', tools, instructions });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/mail.js';
5
5
 
6
- startServer({ name: 'hostinger-mail-mcp', version: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-mail-mcp', version: '1.62.0', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/mail.js';
5
5
 
6
- startServer({ name: 'hostinger-mail-mcp', version: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-mail-mcp', version: '1.62.0', 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: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-reach-mcp', version: '1.62.0', 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: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-reach-mcp', version: '1.62.0', 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: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-vps-mcp', version: '1.62.0', 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: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-vps-mcp', version: '1.62.0', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/wordpress.js';
5
5
 
6
- startServer({ name: 'hostinger-wordpress-mcp', version: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-wordpress-mcp', version: '1.62.0', tools });
@@ -3,4 +3,4 @@
3
3
  import { startServer } from '../core/runtime.js';
4
4
  import tools from '../core/tools/wordpress.js';
5
5
 
6
- startServer({ name: 'hostinger-wordpress-mcp', version: '1.61.1', tools });
6
+ startServer({ name: 'hostinger-wordpress-mcp', version: '1.62.0', tools });