vpsnet-mcp 1.0.8 → 2.1.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.
@@ -0,0 +1,97 @@
1
+ import { z } from "zod";
2
+ export const serviceRescueImageIdSchema = z
3
+ .string()
4
+ .trim()
5
+ .min(1)
6
+ .max(64)
7
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/)
8
+ .describe("Exact opaque rescue image ID returned by get_service_rescue");
9
+ const serviceRescueImageSchema = z.object({
10
+ id: serviceRescueImageIdSchema,
11
+ family: z.enum(["linux", "iso"]),
12
+ console: z.enum(["serial", "novnc"]),
13
+ mountPath: z.string().max(128).optional(),
14
+ device: z.string().max(128).optional(),
15
+ }).strict();
16
+ const serviceRescueCapabilitySchema = z.object({
17
+ supported: z.boolean(),
18
+ enabled: z.boolean(),
19
+ platform: z.string().min(1).max(16),
20
+ images: z.array(serviceRescueImageSchema).max(8),
21
+ }).strict();
22
+ export const serviceRescueSessionSchema = z.object({
23
+ id: z.string().uuid(),
24
+ platform: z.enum(["firecracker", "vds"]),
25
+ image: serviceRescueImageIdSchema,
26
+ state: z.enum([
27
+ "queued",
28
+ "processing",
29
+ "awaiting_reply",
30
+ "active",
31
+ "needs_attention",
32
+ "failed",
33
+ "completed",
34
+ ]),
35
+ active: z.boolean(),
36
+ desiredState: z.enum(["active", "inactive"]),
37
+ operation: z.enum(["enter", "exit"]),
38
+ progressPercent: z.number().int().min(0).max(100),
39
+ progressStage: z.string().min(1).max(64),
40
+ originalRunning: z.boolean().nullable(),
41
+ errorCode: z.string().max(96).nullable(),
42
+ activatedAt: z.string().max(32).nullable(),
43
+ completedAt: z.string().max(32).nullable(),
44
+ createdAt: z.string().min(1).max(32),
45
+ updatedAt: z.string().min(1).max(32),
46
+ }).strict();
47
+ const serviceRescueStatusResponseSchema = z.object({
48
+ success: z.literal(true),
49
+ rescue: z.object({
50
+ capability: serviceRescueCapabilitySchema,
51
+ session: serviceRescueSessionSchema.nullable(),
52
+ }).strict(),
53
+ }).strict();
54
+ const serviceRescueMutationResponseSchema = z.object({
55
+ success: z.literal(true),
56
+ replayed: z.boolean(),
57
+ retried: z.boolean(),
58
+ session: serviceRescueSessionSchema,
59
+ }).strict();
60
+ function record(value) {
61
+ return value && typeof value === "object" && !Array.isArray(value)
62
+ ? value
63
+ : {};
64
+ }
65
+ function errorCodes(value) {
66
+ return Object.entries(record(value))
67
+ .filter(([key, enabled]) => enabled === true
68
+ && !["success", "replayed", "retried"].includes(key)
69
+ && /^[A-Za-z][A-Za-z0-9._-]{0,95}$/.test(key))
70
+ .slice(0, 20)
71
+ .map(([key]) => key);
72
+ }
73
+ export function parseServiceRescueStatus(data) {
74
+ const parsed = serviceRescueStatusResponseSchema.safeParse(data);
75
+ return parsed.success ? parsed.data : null;
76
+ }
77
+ export function safeServiceRescuePayload(status, data) {
78
+ const statusPayload = serviceRescueStatusResponseSchema.safeParse(data);
79
+ if (statusPayload.success) {
80
+ return statusPayload.data;
81
+ }
82
+ const mutationPayload = serviceRescueMutationResponseSchema.safeParse(data);
83
+ if (mutationPayload.success) {
84
+ return mutationPayload.data;
85
+ }
86
+ return {
87
+ success: false,
88
+ http_status: status,
89
+ error_codes: errorCodes(data),
90
+ };
91
+ }
92
+ export function serviceRescueEnterRequestBody(image) {
93
+ return {
94
+ image,
95
+ acknowledgeReboot: true,
96
+ };
97
+ }
@@ -0,0 +1,284 @@
1
+ import { isIP } from "node:net";
2
+ import { z } from "zod";
3
+ export const tempVmIdSchema = z
4
+ .number()
5
+ .int()
6
+ .positive()
7
+ .describe("on-demand server session ID returned by list_temp_vms");
8
+ export const tempVmProfileSchema = z
9
+ .enum(["standard", "power"])
10
+ .describe("on-demand server profile returned by get_temp_vm_options");
11
+ export const tempVmTtlSchema = z
12
+ .number()
13
+ .int()
14
+ .min(30)
15
+ .max(360)
16
+ .describe("Session duration from get_temp_vm_options.allowed_ttls");
17
+ export const tempVmOsIdSchema = z
18
+ .number()
19
+ .int()
20
+ .positive()
21
+ .describe("Optional enabled Firecracker guest OS ID. Omit it to use the current default; Functions runtime images are rejected.");
22
+ export const tempVmRootPasswordSchema = z
23
+ .string()
24
+ .min(6)
25
+ .max(40)
26
+ .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[A-Za-z0-9]+$/)
27
+ .describe("Optional root password with uppercase, lowercase, and numeric characters. Never use together with ssh_public_key.");
28
+ export const tempVmSshPublicKeySchema = z
29
+ .string()
30
+ .min(1)
31
+ .max(8192)
32
+ .describe("Optional SSH public key. Never use together with root_password. Omit both credential fields for password delivery by account email.");
33
+ export const tempVmIdempotencyKeySchema = z
34
+ .string()
35
+ .min(16)
36
+ .max(190)
37
+ .regex(/^[A-Za-z0-9._:-]{16,190}$/)
38
+ .describe("Client-global key for one exact on-demand server quote and create attempt. Reuse it only with the unchanged request.");
39
+ export const tempVmQuoteTokenSchema = z
40
+ .string()
41
+ .min(32)
42
+ .max(190)
43
+ .regex(/^[A-Za-z0-9._:-]{32,190}$/)
44
+ .describe("Short-lived quote token returned by quote_temp_vm");
45
+ const tempVmStatusSchema = z.enum([
46
+ "payment_pending",
47
+ "payment_failed",
48
+ "queued",
49
+ "creating",
50
+ "running",
51
+ "create_failed",
52
+ "destroy_pending",
53
+ "destroying",
54
+ "destroyed",
55
+ ]);
56
+ export const tempVmSmtpBlockedPorts = [25, 2525, 465, 587];
57
+ function record(value) {
58
+ return value && typeof value === "object" && !Array.isArray(value)
59
+ ? value
60
+ : {};
61
+ }
62
+ function stringOrNull(value, maxLength = 255) {
63
+ if (value === null || value === undefined)
64
+ return null;
65
+ if (typeof value !== "string")
66
+ return null;
67
+ const text = value.replace(/[\u0000-\u001f\u007f]/g, " ").trim();
68
+ return text ? text.slice(0, maxLength) : null;
69
+ }
70
+ function ipv4OrNull(value) {
71
+ const candidate = stringOrNull(value, 15);
72
+ return candidate !== null && isIP(candidate) === 4 ? candidate : null;
73
+ }
74
+ function finiteNumber(value) {
75
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
76
+ }
77
+ function booleanOrNull(value) {
78
+ return typeof value === "boolean" ? value : null;
79
+ }
80
+ function positiveInteger(value) {
81
+ return typeof value === "number" && Number.isInteger(value) && value > 0
82
+ ? value
83
+ : null;
84
+ }
85
+ function nullablePositiveInteger(value) {
86
+ return value === null ? null : positiveInteger(value);
87
+ }
88
+ function safeProfile(value) {
89
+ const source = record(value);
90
+ const id = tempVmProfileSchema.safeParse(source.id);
91
+ const planId = positiveInteger(source.plan_id);
92
+ if (!id.success || planId === null)
93
+ return null;
94
+ return {
95
+ id: id.data,
96
+ name: stringOrNull(source.name, 128),
97
+ plan_id: planId,
98
+ plan_name: stringOrNull(source.plan_name, 128),
99
+ multiplier: finiteNumber(source.multiplier),
100
+ default_ttl: positiveInteger(source.default_ttl),
101
+ plan_hourly_ex_vat: finiteNumber(source.plan_hourly_ex_vat),
102
+ };
103
+ }
104
+ function safeOptions(value) {
105
+ const source = record(value);
106
+ const storagePolicy = record(source.storage_policy);
107
+ const profiles = Array.isArray(source.profiles)
108
+ ? source.profiles.map(safeProfile).filter((item) => item !== null)
109
+ : [];
110
+ const allowedTtls = Array.isArray(source.allowed_ttls)
111
+ ? source.allowed_ttls.filter((item) => typeof item === "number" && Number.isInteger(item) && item >= 30 && item <= 360)
112
+ : [];
113
+ const publicIp = record(source.public_ip);
114
+ const smtpBlockedPorts = Array.isArray(publicIp.smtp_blocked_ports)
115
+ ? publicIp.smtp_blocked_ports.filter((port) => typeof port === "number"
116
+ && tempVmSmtpBlockedPorts.includes(port))
117
+ : [];
118
+ const reportsExactSmtpPolicy = smtpBlockedPorts.length === tempVmSmtpBlockedPorts.length
119
+ && tempVmSmtpBlockedPorts.every((port) => smtpBlockedPorts.includes(port));
120
+ if (profiles.length === 0 || allowedTtls.length === 0)
121
+ return null;
122
+ const orderable = source.orderable === true && source.availability === "available";
123
+ return {
124
+ orderable,
125
+ availability: orderable ? "available" : "coming_soon",
126
+ profiles,
127
+ allowed_ttls: allowedTtls,
128
+ hard_max_ttl: positiveInteger(source.hard_max_ttl),
129
+ min_bill: positiveInteger(source.min_bill),
130
+ storage_policy: {
131
+ local_disk_deleted_with_server: booleanOrNull(storagePolicy.local_disk_deleted_with_server),
132
+ automatic_backups_included: booleanOrNull(storagePolicy.automatic_backups_included),
133
+ snapshots_available: booleanOrNull(storagePolicy.snapshots_available),
134
+ },
135
+ public_ip: {
136
+ enabled: publicIp.enabled === true,
137
+ smtp_block_forced: publicIp.smtp_block_forced === true,
138
+ smtp_blocked_ports: reportsExactSmtpPolicy ? [...tempVmSmtpBlockedPorts] : [],
139
+ default_on: publicIp.default_on === true,
140
+ },
141
+ convert_to_monthly_allowed: source.convert_to_monthly_allowed === true,
142
+ max_paid_extensions: typeof source.max_paid_extensions === "number"
143
+ && Number.isInteger(source.max_paid_extensions)
144
+ && source.max_paid_extensions >= 0
145
+ ? source.max_paid_extensions
146
+ : null,
147
+ max_concurrent: positiveInteger(source.max_concurrent),
148
+ };
149
+ }
150
+ function safeSession(value) {
151
+ const source = record(value);
152
+ const id = positiveInteger(source.id);
153
+ const profile = tempVmProfileSchema.safeParse(source.profile);
154
+ const status = tempVmStatusSchema.safeParse(source.status);
155
+ if (id === null || !profile.success || !status.success)
156
+ return null;
157
+ const orderState = source.order_state;
158
+ return {
159
+ id,
160
+ profile: profile.data,
161
+ plan_id: positiveInteger(source.plan_id),
162
+ os_id: positiveInteger(source.os_id),
163
+ order_id: nullablePositiveInteger(source.order_id),
164
+ order_no: stringOrNull(source.order_no, 64),
165
+ order_state: typeof orderState === "number" || typeof orderState === "string"
166
+ ? orderState
167
+ : null,
168
+ ip: ipv4OrNull(source.ip),
169
+ ttl_minutes: positiveInteger(source.ttl_minutes),
170
+ billable_minutes: positiveInteger(source.billable_minutes),
171
+ amount_ex_vat: finiteNumber(source.amount_ex_vat),
172
+ amount_gross: finiteNumber(source.amount_gross),
173
+ public_ip_enabled: source.public_ip_enabled === true,
174
+ status: status.data,
175
+ starts_at: stringOrNull(source.starts_at, 32),
176
+ expires_at: stringOrNull(source.expires_at, 32),
177
+ destroyed_at: stringOrNull(source.destroyed_at, 32),
178
+ destroy_reason: stringOrNull(source.destroy_reason, 96),
179
+ refunded: typeof source.refunded === "boolean" ? source.refunded : null,
180
+ created_at: stringOrNull(source.created_at, 32),
181
+ };
182
+ }
183
+ function safeUsageAccess(value) {
184
+ const source = record(value);
185
+ return {
186
+ can_use: source.can_use === true,
187
+ can_use_services: source.can_use_services === true,
188
+ payment_access_available: source.payment_access_available === true,
189
+ has_saved_card: source.has_saved_card === true,
190
+ card_fallback_allowed: source.card_fallback_allowed === true,
191
+ block_reason: stringOrNull(source.block_reason, 64),
192
+ notice_reason: stringOrNull(source.notice_reason, 64),
193
+ };
194
+ }
195
+ function errorCodes(value) {
196
+ const source = record(value);
197
+ const explicit = typeof source.error === "string" ? [source.error] : [];
198
+ const booleans = Object.entries(source)
199
+ .filter(([, enabled]) => enabled === true)
200
+ .map(([key]) => key);
201
+ return [...new Set([...explicit, ...booleans])]
202
+ .filter((key) => /^[A-Za-z][A-Za-z0-9._-]{0,95}$/.test(key))
203
+ .slice(0, 20);
204
+ }
205
+ function safeQuote(source) {
206
+ const profile = tempVmProfileSchema.safeParse(source.profile);
207
+ const quoteToken = tempVmQuoteTokenSchema.safeParse(source.quoteToken);
208
+ if (!profile.success || !quoteToken.success)
209
+ return null;
210
+ const access = record(source.access);
211
+ return {
212
+ success: true,
213
+ profile: profile.data,
214
+ plan_id: positiveInteger(source.plan_id),
215
+ plan_name: stringOrNull(source.plan_name, 128),
216
+ ttl_minutes: positiveInteger(source.ttl_minutes),
217
+ billable_minutes: positiveInteger(source.billable_minutes),
218
+ min_bill_minutes: positiveInteger(source.min_bill_minutes),
219
+ hard_max_ttl: positiveInteger(source.hard_max_ttl),
220
+ multiplier: finiteNumber(source.multiplier),
221
+ plan_hourly_ex_vat: finiteNumber(source.plan_hourly_ex_vat),
222
+ amount_ex_vat: finiteNumber(source.amount_ex_vat),
223
+ vat_percent: finiteNumber(source.vat_percent),
224
+ amount_gross: finiteNumber(source.amount_gross),
225
+ minimum_charge_applied: source.minimum_charge_applied === true,
226
+ minimum_gross_eur: finiteNumber(source.minimum_gross_eur),
227
+ currency: source.currency === "EUR" ? "EUR" : null,
228
+ public_ip_default: source.public_ip_default === true,
229
+ public_ip_required: source.public_ip_required === true,
230
+ extensions: typeof source.extensions === "number" && Number.isInteger(source.extensions)
231
+ ? source.extensions
232
+ : null,
233
+ access: {
234
+ allowed: access.allowed === true,
235
+ reason: stringOrNull(access.reason, 64),
236
+ balance: finiteNumber(access.balance),
237
+ usageAccess: safeUsageAccess(access.usageAccess),
238
+ },
239
+ quoteToken: quoteToken.data,
240
+ quoteExpiresAt: stringOrNull(source.quoteExpiresAt, 32),
241
+ };
242
+ }
243
+ export function safeTempVmPayload(status, data) {
244
+ const source = record(data);
245
+ if (source.success === true) {
246
+ if (source.quoteToken !== undefined) {
247
+ const quote = safeQuote(source);
248
+ if (quote !== null)
249
+ return quote;
250
+ }
251
+ if (Array.isArray(source.sessions)) {
252
+ const options = safeOptions(source.options);
253
+ if (options !== null) {
254
+ return {
255
+ success: true,
256
+ sessions: source.sessions
257
+ .map(safeSession)
258
+ .filter((item) => item !== null),
259
+ options,
260
+ };
261
+ }
262
+ }
263
+ const session = safeSession(source.session);
264
+ if (session !== null) {
265
+ const payload = { success: true, session };
266
+ if (typeof source.replayed === "boolean") {
267
+ payload.replayed = source.replayed;
268
+ }
269
+ return payload;
270
+ }
271
+ const options = safeOptions(source.options);
272
+ if (options !== null)
273
+ return { success: true, options };
274
+ }
275
+ return {
276
+ success: false,
277
+ http_status: status,
278
+ error_codes: errorCodes(source),
279
+ required_scope: stringOrNull(source.requiredScope, 64),
280
+ retry_after_seconds: typeof source.retryAfter === "number" && Number.isInteger(source.retryAfter)
281
+ ? source.retryAfter
282
+ : null,
283
+ };
284
+ }
package/package.json CHANGED
@@ -1,18 +1,28 @@
1
1
  {
2
2
  "name": "vpsnet-mcp",
3
- "version": "1.0.8",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
- "description": "MCP server for managing VPSnet.com virtual private servers",
5
+ "description": "MCP server for managing VPSnet.com services, applications, DNS, domains, billing, and server lifecycle operations",
6
6
  "bin": {
7
7
  "vpsnet-mcp": "./build/index.js"
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsc",
11
11
  "dev": "tsc --watch",
12
+ "test": "npm run build && node --test test/*.test.mjs",
12
13
  "prepublishOnly": "npm run build"
13
14
  },
14
15
  "files": [
15
- "build"
16
+ "build/api-configuration.js",
17
+ "build/api.js",
18
+ "build/application-contract.js",
19
+ "build/auth-failure.js",
20
+ "build/certificate-contract.js",
21
+ "build/custom-project-contract.js",
22
+ "build/file-restore-contract.js",
23
+ "build/index.js",
24
+ "build/service-rescue-contract.js",
25
+ "build/temp-vm-contract.js"
16
26
  ],
17
27
  "repository": "github:bacarrdy/vpsnet-mcp",
18
28
  "homepage": "https://github.com/bacarrdy/vpsnet-mcp",
@@ -21,17 +31,22 @@
21
31
  "keywords": [
22
32
  "mcp",
23
33
  "vps",
34
+ "dns",
35
+ "domains",
24
36
  "vpsnet",
25
37
  "model-context-protocol",
26
38
  "claude",
27
39
  "server-management",
28
- "hosting"
40
+ "hosting",
41
+ "managed-applications",
42
+ "docker-applications",
43
+ "domain-registration"
29
44
  ],
30
45
  "engines": {
31
- "node": ">=18"
46
+ "node": ">=20"
32
47
  },
33
48
  "dependencies": {
34
- "@modelcontextprotocol/sdk": "^1.12.1",
49
+ "@modelcontextprotocol/sdk": "^1.30.0",
35
50
  "zod": "^3.24.2"
36
51
  },
37
52
  "devDependencies": {