hostinger-api-mcp 1.63.3 → 2.0.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 +23 -0
- package/README.md +32 -21
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/headless/SKILL.md +5 -5
- package/skills/headless/references/DATABASE.md +2 -2
- package/skills/headless/references/DEPLOYMENT.md +1 -1
- package/skills/headless/references/SETUP.md +3 -3
- package/skills/headless/references/STORE.md +2 -2
- package/skills/headless/references/WORDPRESS.md +1 -1
- package/src/core/catalog.js +183 -0
- package/src/core/catalog.ts +207 -0
- package/src/core/instructions.js +1 -1
- package/src/core/instructions.ts +1 -1
- package/src/core/runtime.js +62 -86
- package/src/core/runtime.ts +53 -88
- package/src/core/tools/all.js +2 -2
- package/src/core/tools/all.ts +2 -2
- package/src/core/tools/hosting.js +2 -2
- package/src/core/tools/hosting.ts +2 -2
- package/src/servers/agency-hosting.js +1 -1
- package/src/servers/agency-hosting.ts +1 -1
- 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/ecommerce.js +1 -1
- package/src/servers/ecommerce.ts +1 -1
- package/src/servers/horizons.js +1 -1
- package/src/servers/horizons.ts +1 -1
- package/src/servers/hosting.js +1 -1
- package/src/servers/hosting.ts +1 -1
- package/src/servers/mail.js +1 -1
- package/src/servers/mail.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/servers/wordpress.js +1 -1
- package/src/servers/wordpress.ts +1 -1
- package/types.d.ts +5 -2
package/src/core/instructions.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated server instructions
|
|
2
|
-
export default "Hostinger API notes for agents.
|
|
2
|
+
export default "Hostinger API notes for agents. Find operations with search; the inputSchema it returns is authoritative for request shapes. Run one with execute, or chain several with multi-execute.\n\nDatabases (web hosting, hosting_* operations)\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_* operations) 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 operation 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";
|
package/src/core/instructions.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Auto-generated server instructions
|
|
2
|
-
const instructions: string = "Hostinger API notes for agents.
|
|
2
|
+
const instructions: string = "Hostinger API notes for agents. Find operations with search; the inputSchema it returns is authoritative for request shapes. Run one with execute, or chain several with multi-execute.\n\nDatabases (web hosting, hosting_* operations)\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_* operations) 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 operation 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
3
|
export default instructions;
|
package/src/core/runtime.js
CHANGED
|
@@ -17,6 +17,7 @@ import { OAuthProvider, getEnvToken } from "./oauth.js";
|
|
|
17
17
|
import * as tus from "tus-js-client";
|
|
18
18
|
import fs from "fs";
|
|
19
19
|
import path from "path";
|
|
20
|
+
import { META_TOOLS, searchOperations, renderSearch, findOperation, runSteps } from "./catalog.js";
|
|
20
21
|
|
|
21
22
|
// Load environment variables
|
|
22
23
|
dotenvConfig({ quiet: true });
|
|
@@ -44,7 +45,7 @@ const SECURITY_SCHEMES = {
|
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* MCP Server for Hostinger API
|
|
47
|
-
* Generated from OpenAPI spec version 1.54.
|
|
48
|
+
* Generated from OpenAPI spec version 1.54.2
|
|
48
49
|
*/
|
|
49
50
|
class MCPServer {
|
|
50
51
|
constructor({ name, version, tools, instructions }) {
|
|
@@ -54,7 +55,7 @@ class MCPServer {
|
|
|
54
55
|
this.toolList = tools;
|
|
55
56
|
this.instructions = instructions;
|
|
56
57
|
this.server = null;
|
|
57
|
-
this.
|
|
58
|
+
this.operations = new Map();
|
|
58
59
|
this.debug = process.env.DEBUG === "true";
|
|
59
60
|
this.baseUrl = process.env.API_BASE_URL || "https://developers.hostinger.com";
|
|
60
61
|
this.headers = this.parseHeaders(process.env.API_HEADERS || "");
|
|
@@ -115,20 +116,13 @@ class MCPServer {
|
|
|
115
116
|
* This runs before the server is connected, so don't log here
|
|
116
117
|
*/
|
|
117
118
|
initializeTools() {
|
|
118
|
-
//
|
|
119
|
+
// The operations are the catalog behind search/execute; they are not listed as tools.
|
|
119
120
|
for (const tool of this.toolList) {
|
|
120
|
-
this.
|
|
121
|
-
name: tool.name,
|
|
122
|
-
title: tool.title,
|
|
123
|
-
description: tool.description,
|
|
124
|
-
inputSchema: tool.inputSchema,
|
|
125
|
-
annotations: tool.annotations,
|
|
126
|
-
// Don't include security at the tool level
|
|
127
|
-
});
|
|
121
|
+
this.operations.set(tool.name, tool);
|
|
128
122
|
}
|
|
129
123
|
|
|
130
124
|
// Don't log here, we're not connected yet
|
|
131
|
-
console.error(`
|
|
125
|
+
console.error(`Loaded ${this.operations.size} operations`);
|
|
132
126
|
}
|
|
133
127
|
|
|
134
128
|
/**
|
|
@@ -149,97 +143,79 @@ class MCPServer {
|
|
|
149
143
|
// Handle tool listing requests
|
|
150
144
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
151
145
|
this.log('debug', "Handling ListTools request");
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
tools: Array.from(this.tools.entries()).map(([id, tool]) => ({
|
|
155
|
-
id,
|
|
156
|
-
...tool,
|
|
157
|
-
})),
|
|
158
|
-
};
|
|
146
|
+
return { tools: META_TOOLS };
|
|
159
147
|
});
|
|
160
148
|
|
|
161
|
-
// Handle tool execution requests
|
|
162
149
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
163
|
-
const { name
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
let toolName;
|
|
167
|
-
let toolDetails;
|
|
168
|
-
|
|
169
|
-
// Find the requested tool
|
|
170
|
-
for (const [tid, tool] of this.tools.entries()) {
|
|
171
|
-
if (tool.name === name) {
|
|
172
|
-
toolName = name;
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
150
|
+
const { name } = request.params;
|
|
151
|
+
const args = request.params.arguments ?? {};
|
|
152
|
+
this.log('debug', "Handling CallTool request", { name, args });
|
|
176
153
|
|
|
177
154
|
// An unknown tool is a protocol error, not a tool execution error: the
|
|
178
155
|
// 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
|
-
|
|
181
|
-
if (!toolName) {
|
|
156
|
+
// have. -32602 is what the spec's example and the SDK's own McpServer use.
|
|
157
|
+
if (!META_TOOLS.some(t => t.name === name)) {
|
|
182
158
|
throw new McpError(ErrorCode.InvalidParams, `Tool not found: ${name}`);
|
|
183
159
|
}
|
|
184
160
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
throw new McpError(ErrorCode.InternalError, `Tool details not found for ID: ${toolName}`);
|
|
188
|
-
}
|
|
189
|
-
|
|
161
|
+
// Everything below is a tool execution: failures (bad input, unknown
|
|
162
|
+
// operation, API errors) come back inside the result with isError: true.
|
|
190
163
|
try {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (toolDetails.custom) {
|
|
196
|
-
result = await this.executeCustomTool(toolDetails, params || {});
|
|
197
|
-
} else {
|
|
198
|
-
result = await this.executeApiCall(toolDetails, params || {});
|
|
164
|
+
if (name === 'search') {
|
|
165
|
+
const results = searchOperations(this.toolList, args.query, args.limit ?? undefined);
|
|
166
|
+
return { content: [{ type: "text", text: renderSearch(args.query, results) }] };
|
|
199
167
|
}
|
|
200
168
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
content: [
|
|
204
|
-
{
|
|
205
|
-
type: "text",
|
|
206
|
-
text: JSON.stringify(result)
|
|
207
|
-
}
|
|
208
|
-
]
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
} catch (error) {
|
|
212
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
213
|
-
this.log('error', `Error executing tool ${name}: ${errorMessage}`);
|
|
214
|
-
|
|
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;
|
|
169
|
+
if (name === 'execute') {
|
|
170
|
+
const result = await this.executeOperation(args.operation, args.params ?? {});
|
|
171
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
228
172
|
}
|
|
229
173
|
|
|
174
|
+
const batch = await runSteps(
|
|
175
|
+
args.steps,
|
|
176
|
+
(operation, params) => this.executeOperation(operation, params),
|
|
177
|
+
(error) => this.formatToolError(error),
|
|
178
|
+
);
|
|
230
179
|
return {
|
|
231
|
-
isError: true,
|
|
232
|
-
content: [
|
|
233
|
-
{
|
|
234
|
-
type: "text",
|
|
235
|
-
text: errorText
|
|
236
|
-
}
|
|
237
|
-
]
|
|
180
|
+
...(batch.error ? { isError: true } : {}),
|
|
181
|
+
content: [{ type: "text", text: JSON.stringify(batch) }],
|
|
238
182
|
};
|
|
183
|
+
} catch (error) {
|
|
184
|
+
const errorText = this.formatToolError(error);
|
|
185
|
+
this.log('error', `Error executing ${name}: ${errorText}`);
|
|
186
|
+
return { isError: true, content: [{ type: "text", text: errorText }] };
|
|
239
187
|
}
|
|
240
188
|
});
|
|
241
189
|
}
|
|
242
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Look an operation up in the catalog, check its required params, and run it
|
|
193
|
+
* as either a custom tool or a plain API call.
|
|
194
|
+
*/
|
|
195
|
+
async executeOperation(operation, params) {
|
|
196
|
+
const tool = findOperation(this.operations, operation, params);
|
|
197
|
+
this.log('info', `Executing operation: ${tool.name}`);
|
|
198
|
+
return tool.custom
|
|
199
|
+
? this.executeCustomTool(tool, params)
|
|
200
|
+
: this.executeApiCall(tool, params);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Text for an isError result. The status stays in the text so the model can
|
|
205
|
+
* tell a retryable 429/503 from a 403 it should not retry.
|
|
206
|
+
*/
|
|
207
|
+
formatToolError(error) {
|
|
208
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
209
|
+
if (!(error instanceof ApiExecutionError)) {
|
|
210
|
+
return errorMessage;
|
|
211
|
+
}
|
|
212
|
+
const payload = error.responseData;
|
|
213
|
+
const body = payload === null || payload === undefined || payload === ''
|
|
214
|
+
? errorMessage
|
|
215
|
+
: typeof payload === 'object' ? JSON.stringify(payload) : String(payload);
|
|
216
|
+
return error.status ? `HTTP ${error.status}: ${body}` : body;
|
|
217
|
+
}
|
|
218
|
+
|
|
243
219
|
async executeCustomTool(tool, params) {
|
|
244
220
|
switch (tool.name) {
|
|
245
221
|
case 'agency-hosting_deployNodeStaticWebsite':
|
|
@@ -2625,7 +2601,7 @@ class MCPServer {
|
|
|
2625
2601
|
|
|
2626
2602
|
// Start the server
|
|
2627
2603
|
const server = app.listen(port, host, () => {
|
|
2628
|
-
this.log('info', `MCP Server with HTTP streaming transport started successfully with ${
|
|
2604
|
+
this.log('info', `MCP Server with HTTP streaming transport started successfully with ${META_TOOLS.length} tools`);
|
|
2629
2605
|
this.log('info', `Listening on http://${host}:${port}`);
|
|
2630
2606
|
});
|
|
2631
2607
|
|
|
@@ -2651,8 +2627,8 @@ class MCPServer {
|
|
|
2651
2627
|
await this.server.connect(transport);
|
|
2652
2628
|
|
|
2653
2629
|
// Now we can safely log via MCP
|
|
2654
|
-
console.error(`Registered ${
|
|
2655
|
-
this.log('info', `MCP Server with stdio transport started successfully with ${
|
|
2630
|
+
console.error(`Registered ${META_TOOLS.length} tools`);
|
|
2631
|
+
this.log('info', `MCP Server with stdio transport started successfully with ${META_TOOLS.length} tools`);
|
|
2656
2632
|
} catch (error) {
|
|
2657
2633
|
console.error("Failed to start MCP server:", error);
|
|
2658
2634
|
process.exit(1);
|
package/src/core/runtime.ts
CHANGED
|
@@ -19,6 +19,8 @@ import axios,{ AxiosRequestConfig, AxiosError, AxiosResponse } from "axios";
|
|
|
19
19
|
import * as tus from "tus-js-client";
|
|
20
20
|
import fs from "fs";
|
|
21
21
|
import path from "path";
|
|
22
|
+
import { META_TOOLS, searchOperations, renderSearch, findOperation, runSteps } from "./catalog.js";
|
|
23
|
+
import type { CatalogOperation } from "./catalog.js";
|
|
22
24
|
|
|
23
25
|
// Load environment variables
|
|
24
26
|
dotenvConfig({ quiet: true });
|
|
@@ -70,14 +72,14 @@ const SECURITY_SCHEMES: Record<string, SecurityScheme> = {
|
|
|
70
72
|
|
|
71
73
|
/**
|
|
72
74
|
* MCP Server for Hostinger API
|
|
73
|
-
* Generated from OpenAPI spec version 1.54.
|
|
75
|
+
* Generated from OpenAPI spec version 1.54.2
|
|
74
76
|
*/
|
|
75
77
|
class MCPServer {
|
|
76
78
|
private readonly name: string;
|
|
77
79
|
private readonly version: string;
|
|
78
80
|
private readonly toolList: OpenApiTool[];
|
|
79
81
|
private server: Server;
|
|
80
|
-
private
|
|
82
|
+
private operations: Map<string, OpenApiTool> = new Map();
|
|
81
83
|
private instructions?: string;
|
|
82
84
|
private debug: boolean;
|
|
83
85
|
private baseUrl: string;
|
|
@@ -150,20 +152,13 @@ class MCPServer {
|
|
|
150
152
|
* This runs before the server is connected, so don't log here
|
|
151
153
|
*/
|
|
152
154
|
private initializeTools(): void {
|
|
153
|
-
//
|
|
155
|
+
// The operations are the catalog behind search/execute; they are not listed as tools.
|
|
154
156
|
for (const tool of this.toolList) {
|
|
155
|
-
this.
|
|
156
|
-
name: tool.name,
|
|
157
|
-
title: tool.title,
|
|
158
|
-
description: tool.description,
|
|
159
|
-
inputSchema: tool.inputSchema,
|
|
160
|
-
annotations: tool.annotations,
|
|
161
|
-
// Don't include security at the tool level
|
|
162
|
-
});
|
|
157
|
+
this.operations.set(tool.name, tool);
|
|
163
158
|
}
|
|
164
159
|
|
|
165
160
|
// Don't log here, we're not connected yet
|
|
166
|
-
console.error(`
|
|
161
|
+
console.error(`Loaded ${this.operations.size} operations`);
|
|
167
162
|
}
|
|
168
163
|
|
|
169
164
|
/**
|
|
@@ -184,97 +179,67 @@ class MCPServer {
|
|
|
184
179
|
// Handle tool listing requests
|
|
185
180
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
186
181
|
this.log('debug', "Handling ListTools request");
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
tools: Array.from(this.tools.entries()).map(([id, tool]) => ({
|
|
190
|
-
id,
|
|
191
|
-
...tool,
|
|
192
|
-
})),
|
|
193
|
-
};
|
|
182
|
+
return { tools: META_TOOLS };
|
|
194
183
|
});
|
|
195
184
|
|
|
196
|
-
// Handle tool execution requests
|
|
197
185
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
198
|
-
const { name
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
let toolName: string | undefined;
|
|
202
|
-
let toolDetails: OpenApiTool | undefined;
|
|
203
|
-
|
|
204
|
-
// Find the requested tool
|
|
205
|
-
for (const [tid, tool] of this.tools.entries()) {
|
|
206
|
-
if (tool.name === name) {
|
|
207
|
-
toolName = name;
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
186
|
+
const { name } = request.params;
|
|
187
|
+
const args = (request.params.arguments ?? {}) as Record<string, any>;
|
|
188
|
+
this.log('debug', "Handling CallTool request", { name, args });
|
|
211
189
|
|
|
212
|
-
|
|
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.
|
|
216
|
-
if (!toolName) {
|
|
190
|
+
if (!META_TOOLS.some(t => t.name === name)) {
|
|
217
191
|
throw new McpError(ErrorCode.InvalidParams, `Tool not found: ${name}`);
|
|
218
192
|
}
|
|
219
193
|
|
|
220
|
-
toolDetails = this.toolList.find(t => t.name === toolName);
|
|
221
|
-
if (!toolDetails) {
|
|
222
|
-
throw new McpError(ErrorCode.InternalError, `Tool details not found for ID: ${toolName}`);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
194
|
try {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (toolDetails.custom) {
|
|
231
|
-
result = await this.executeCustomTool(toolDetails, params || {});
|
|
232
|
-
} else {
|
|
233
|
-
result = await this.executeApiCall(toolDetails, params || {});
|
|
195
|
+
if (name === 'search') {
|
|
196
|
+
const results = searchOperations(this.toolList, args.query, args.limit ?? undefined);
|
|
197
|
+
return { content: [{ type: "text", text: renderSearch(String(args.query), results) }] };
|
|
234
198
|
}
|
|
235
199
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
content: [
|
|
239
|
-
{
|
|
240
|
-
type: "text",
|
|
241
|
-
text: JSON.stringify(result)
|
|
242
|
-
}
|
|
243
|
-
]
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
} catch (error) {
|
|
247
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
248
|
-
this.log('error', `Error executing tool ${toolName}: ${errorMessage}`);
|
|
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;
|
|
200
|
+
if (name === 'execute') {
|
|
201
|
+
const result = await this.executeOperation(args.operation, args.params ?? {});
|
|
202
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
263
203
|
}
|
|
264
204
|
|
|
205
|
+
const batch = await runSteps(
|
|
206
|
+
args.steps,
|
|
207
|
+
(operation, params) => this.executeOperation(operation, params),
|
|
208
|
+
(error) => this.formatToolError(error),
|
|
209
|
+
);
|
|
265
210
|
return {
|
|
266
|
-
isError: true,
|
|
267
|
-
content: [
|
|
268
|
-
{
|
|
269
|
-
type: "text",
|
|
270
|
-
text: errorText
|
|
271
|
-
}
|
|
272
|
-
]
|
|
211
|
+
...(batch.error ? { isError: true } : {}),
|
|
212
|
+
content: [{ type: "text", text: JSON.stringify(batch) }],
|
|
273
213
|
};
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const errorText = this.formatToolError(error);
|
|
216
|
+
this.log('error', `Error executing ${name}: ${errorText}`);
|
|
217
|
+
return { isError: true, content: [{ type: "text", text: errorText }] };
|
|
274
218
|
}
|
|
275
219
|
});
|
|
276
220
|
}
|
|
277
221
|
|
|
222
|
+
private async executeOperation(operation: unknown, params: unknown): Promise<any> {
|
|
223
|
+
const tool = findOperation(this.operations, operation, params);
|
|
224
|
+
const input = params as Record<string, any>;
|
|
225
|
+
this.log('info', `Executing operation: ${tool.name}`);
|
|
226
|
+
return tool.custom
|
|
227
|
+
? this.executeCustomTool(tool, input)
|
|
228
|
+
: this.executeApiCall(tool, input);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private formatToolError(error: unknown): string {
|
|
232
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
233
|
+
if (!(error instanceof ApiExecutionError)) {
|
|
234
|
+
return errorMessage;
|
|
235
|
+
}
|
|
236
|
+
const payload = error.responseData;
|
|
237
|
+
const body = payload === null || payload === undefined || payload === ''
|
|
238
|
+
? errorMessage
|
|
239
|
+
: typeof payload === 'object' ? JSON.stringify(payload) : String(payload);
|
|
240
|
+
return error.status ? `HTTP ${error.status}: ${body}` : body;
|
|
241
|
+
}
|
|
242
|
+
|
|
278
243
|
private async executeCustomTool(tool: OpenApiTool, params: Record<string, any>): Promise<any> {
|
|
279
244
|
switch (tool.name) {
|
|
280
245
|
case 'agency-hosting_deployNodeStaticWebsite':
|
|
@@ -2681,7 +2646,7 @@ class MCPServer {
|
|
|
2681
2646
|
|
|
2682
2647
|
// Start the server
|
|
2683
2648
|
const server = app.listen(port, host, () => {
|
|
2684
|
-
this.log('info', `MCP Server with HTTP streaming transport started successfully with ${
|
|
2649
|
+
this.log('info', `MCP Server with HTTP streaming transport started successfully with ${META_TOOLS.length} tools`);
|
|
2685
2650
|
this.log('info', `Listening on http://${host}:${port}`);
|
|
2686
2651
|
});
|
|
2687
2652
|
|
|
@@ -2707,8 +2672,8 @@ class MCPServer {
|
|
|
2707
2672
|
await this.server.connect(transport);
|
|
2708
2673
|
|
|
2709
2674
|
// Now we can safely log via MCP
|
|
2710
|
-
console.error(`Registered ${
|
|
2711
|
-
this.log('info', `MCP Server with stdio transport started successfully with ${
|
|
2675
|
+
console.error(`Registered ${META_TOOLS.length} tools`);
|
|
2676
|
+
this.log('info', `MCP Server with stdio transport started successfully with ${META_TOOLS.length} tools`);
|
|
2712
2677
|
} catch (error) {
|
|
2713
2678
|
console.error("Failed to start MCP server:", error);
|
|
2714
2679
|
process.exit(1);
|
package/src/core/tools/all.js
CHANGED
|
@@ -8566,7 +8566,7 @@ export default [
|
|
|
8566
8566
|
"readOnlyHint": false,
|
|
8567
8567
|
"destructiveHint": false
|
|
8568
8568
|
},
|
|
8569
|
-
"description": "Create a new website for the authenticated client.\n\
|
|
8569
|
+
"description": "Create a new website for the authenticated client.\n\nYou must choose which hosting order to create this website on. Pass that\norder as `order_id` together with the domain name. List orders to see\navailable IDs; the website is provisioned on that order's hosting plan.\n\nThe datacenter_code parameter is required when creating the first website\non a new hosting plan - this will set up and configure new hosting account\nin the selected datacenter.\n\nSubsequent websites will be hosted on the same datacenter automatically.\n\nWebsite creation takes up to a few minutes to complete. Check the\nwebsites list endpoint to see when your new website becomes available.",
|
|
8570
8570
|
"method": "POST",
|
|
8571
8571
|
"path": "/api/hosting/v1/websites",
|
|
8572
8572
|
"inputSchema": {
|
|
@@ -8578,7 +8578,7 @@ export default [
|
|
|
8578
8578
|
},
|
|
8579
8579
|
"order_id": {
|
|
8580
8580
|
"type": "integer",
|
|
8581
|
-
"description": "ID
|
|
8581
|
+
"description": "Hosting order ID to create this website on. Choose the order whose hosting plan should host the new website. List orders to find available IDs."
|
|
8582
8582
|
},
|
|
8583
8583
|
"datacenter_code": {
|
|
8584
8584
|
"type": "string",
|
package/src/core/tools/all.ts
CHANGED
|
@@ -8580,7 +8580,7 @@ const tools: OpenApiTool[] = [
|
|
|
8580
8580
|
"readOnlyHint": false,
|
|
8581
8581
|
"destructiveHint": false
|
|
8582
8582
|
},
|
|
8583
|
-
"description": "Create a new website for the authenticated client.\n\
|
|
8583
|
+
"description": "Create a new website for the authenticated client.\n\nYou must choose which hosting order to create this website on. Pass that\norder as `order_id` together with the domain name. List orders to see\navailable IDs; the website is provisioned on that order's hosting plan.\n\nThe datacenter_code parameter is required when creating the first website\non a new hosting plan - this will set up and configure new hosting account\nin the selected datacenter.\n\nSubsequent websites will be hosted on the same datacenter automatically.\n\nWebsite creation takes up to a few minutes to complete. Check the\nwebsites list endpoint to see when your new website becomes available.",
|
|
8584
8584
|
"method": "POST",
|
|
8585
8585
|
"path": "/api/hosting/v1/websites",
|
|
8586
8586
|
"inputSchema": {
|
|
@@ -8592,7 +8592,7 @@ const tools: OpenApiTool[] = [
|
|
|
8592
8592
|
},
|
|
8593
8593
|
"order_id": {
|
|
8594
8594
|
"type": "integer",
|
|
8595
|
-
"description": "ID
|
|
8595
|
+
"description": "Hosting order ID to create this website on. Choose the order whose hosting plan should host the new website. List orders to find available IDs."
|
|
8596
8596
|
},
|
|
8597
8597
|
"datacenter_code": {
|
|
8598
8598
|
"type": "string",
|
|
@@ -3105,7 +3105,7 @@ export default [
|
|
|
3105
3105
|
"readOnlyHint": false,
|
|
3106
3106
|
"destructiveHint": false
|
|
3107
3107
|
},
|
|
3108
|
-
"description": "Create a new website for the authenticated client.\n\
|
|
3108
|
+
"description": "Create a new website for the authenticated client.\n\nYou must choose which hosting order to create this website on. Pass that\norder as `order_id` together with the domain name. List orders to see\navailable IDs; the website is provisioned on that order's hosting plan.\n\nThe datacenter_code parameter is required when creating the first website\non a new hosting plan - this will set up and configure new hosting account\nin the selected datacenter.\n\nSubsequent websites will be hosted on the same datacenter automatically.\n\nWebsite creation takes up to a few minutes to complete. Check the\nwebsites list endpoint to see when your new website becomes available.",
|
|
3109
3109
|
"method": "POST",
|
|
3110
3110
|
"path": "/api/hosting/v1/websites",
|
|
3111
3111
|
"inputSchema": {
|
|
@@ -3117,7 +3117,7 @@ export default [
|
|
|
3117
3117
|
},
|
|
3118
3118
|
"order_id": {
|
|
3119
3119
|
"type": "integer",
|
|
3120
|
-
"description": "ID
|
|
3120
|
+
"description": "Hosting order ID to create this website on. Choose the order whose hosting plan should host the new website. List orders to find available IDs."
|
|
3121
3121
|
},
|
|
3122
3122
|
"datacenter_code": {
|
|
3123
3123
|
"type": "string",
|
|
@@ -3119,7 +3119,7 @@ const tools: OpenApiTool[] = [
|
|
|
3119
3119
|
"readOnlyHint": false,
|
|
3120
3120
|
"destructiveHint": false
|
|
3121
3121
|
},
|
|
3122
|
-
"description": "Create a new website for the authenticated client.\n\
|
|
3122
|
+
"description": "Create a new website for the authenticated client.\n\nYou must choose which hosting order to create this website on. Pass that\norder as `order_id` together with the domain name. List orders to see\navailable IDs; the website is provisioned on that order's hosting plan.\n\nThe datacenter_code parameter is required when creating the first website\non a new hosting plan - this will set up and configure new hosting account\nin the selected datacenter.\n\nSubsequent websites will be hosted on the same datacenter automatically.\n\nWebsite creation takes up to a few minutes to complete. Check the\nwebsites list endpoint to see when your new website becomes available.",
|
|
3123
3123
|
"method": "POST",
|
|
3124
3124
|
"path": "/api/hosting/v1/websites",
|
|
3125
3125
|
"inputSchema": {
|
|
@@ -3131,7 +3131,7 @@ const tools: OpenApiTool[] = [
|
|
|
3131
3131
|
},
|
|
3132
3132
|
"order_id": {
|
|
3133
3133
|
"type": "integer",
|
|
3134
|
-
"description": "ID
|
|
3134
|
+
"description": "Hosting order ID to create this website on. Choose the order whose hosting plan should host the new website. List orders to find available IDs."
|
|
3135
3135
|
},
|
|
3136
3136
|
"datacenter_code": {
|
|
3137
3137
|
"type": "string",
|
|
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
|
|
|
4
4
|
import tools from '../core/tools/agency-hosting.js';
|
|
5
5
|
import instructions from '../core/instructions.js';
|
|
6
6
|
|
|
7
|
-
startServer({ name: 'hostinger-agency-hosting-mcp', version: '
|
|
7
|
+
startServer({ name: 'hostinger-agency-hosting-mcp', version: '2.0.0', tools, instructions });
|
|
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
|
|
|
4
4
|
import tools from '../core/tools/agency-hosting.js';
|
|
5
5
|
import instructions from '../core/instructions.js';
|
|
6
6
|
|
|
7
|
-
startServer({ name: 'hostinger-agency-hosting-mcp', version: '
|
|
7
|
+
startServer({ name: 'hostinger-agency-hosting-mcp', version: '2.0.0', tools, instructions });
|
package/src/servers/all.js
CHANGED
|
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
|
|
|
4
4
|
import tools from '../core/tools/all.js';
|
|
5
5
|
import instructions from '../core/instructions.js';
|
|
6
6
|
|
|
7
|
-
startServer({ name: 'hostinger-api-mcp', version: '
|
|
7
|
+
startServer({ name: 'hostinger-api-mcp', version: '2.0.0', tools, instructions });
|
package/src/servers/all.ts
CHANGED
|
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
|
|
|
4
4
|
import tools from '../core/tools/all.js';
|
|
5
5
|
import instructions from '../core/instructions.js';
|
|
6
6
|
|
|
7
|
-
startServer({ name: 'hostinger-api-mcp', version: '
|
|
7
|
+
startServer({ name: 'hostinger-api-mcp', version: '2.0.0', tools, instructions });
|
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/ecommerce.js
CHANGED
package/src/servers/ecommerce.ts
CHANGED
package/src/servers/horizons.js
CHANGED