vpsnet-mcp 2.3.15 → 2.6.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/README.md +57 -11
- package/build/api.js +44 -0
- package/build/dns-zone-contract.js +11 -0
- package/build/index.js +127 -23
- package/build/networking-tools.js +71 -14
- package/build/read-only-context.js +114 -0
- package/build/remote-orders.js +11 -5
- package/build/remote-policy.js +177 -7
- package/build/remote-tools.js +1 -0
- package/build/service-files.js +583 -0
- package/build/service-readiness.js +130 -0
- package/build/service-reinstall-contract.js +8 -0
- package/build/tool-result.js +12 -1
- package/package.json +6 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const orderNo = z.string().regex(/^[A-Za-z0-9-]{1,64}$/);
|
|
3
|
+
const attemptKey = z.string().regex(/^[A-Za-z0-9._:-]{16,128}$/);
|
|
4
|
+
/** Advisory continuation, never an ETA, grant, background task or payment retry. */
|
|
5
|
+
export const serviceFollowUpSchema = z.object({
|
|
6
|
+
purchase_state: z.enum(['accepted', 'pending', 'unknown']).nullable(),
|
|
7
|
+
provisioning_state: z.enum(['pending', 'complete', 'not_running', 'needs_attention', 'failed', 'unknown']),
|
|
8
|
+
permission_required: z.literal('services:read').nullable(),
|
|
9
|
+
order_no: orderNo.nullable(),
|
|
10
|
+
idempotency_key: attemptKey.nullable(),
|
|
11
|
+
next_check: z.object({
|
|
12
|
+
tool: z.enum(['get_service', 'get_purchase_result']),
|
|
13
|
+
arguments: z.object({ orderNo: orderNo.optional(), idempotencyKey: attemptKey.optional() }),
|
|
14
|
+
}).nullable(),
|
|
15
|
+
poll_after_seconds: z.literal(10).nullable(),
|
|
16
|
+
max_checks_before_update: z.literal(6),
|
|
17
|
+
do_not_reorder: z.literal(true),
|
|
18
|
+
background_notification: z.literal(false),
|
|
19
|
+
server_running: z.boolean().nullable(),
|
|
20
|
+
guest_agent_available: z.boolean().nullable(),
|
|
21
|
+
application_ready: z.null(),
|
|
22
|
+
message: z.string(),
|
|
23
|
+
});
|
|
24
|
+
// Directory-only clients must not discover purchase tools through this schema.
|
|
25
|
+
export const serviceResultOutput = z.object({ follow_up: serviceFollowUpSchema.extend({
|
|
26
|
+
next_check: z.object({ tool: z.literal('get_service'), arguments: z.object({ orderNo }) }).nullable(),
|
|
27
|
+
}) }).passthrough();
|
|
28
|
+
export const purchaseResultOutput = z.object({ follow_up: serviceFollowUpSchema.optional() }).passthrough();
|
|
29
|
+
function object(value) {
|
|
30
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
31
|
+
? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
function base() {
|
|
34
|
+
return {
|
|
35
|
+
purchase_state: null, provisioning_state: 'unknown', permission_required: null,
|
|
36
|
+
order_no: null, idempotency_key: null, next_check: null, poll_after_seconds: null,
|
|
37
|
+
max_checks_before_update: 6, do_not_reorder: true, background_notification: false,
|
|
38
|
+
server_running: null, guest_agent_available: null, application_ready: null,
|
|
39
|
+
message: 'Readiness is unknown. Inspect the original order; do not place another order or restart it.',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const pendingMessage = 'Keep the same order and check again after about 10 seconds; this is a polling interval, not a completion estimate. Check at most 6 times before a progress update. If still pending, report that state and retain the order number for the next check. Do not reorder or restart. No background notification is scheduled.';
|
|
43
|
+
export function serviceReadiness(data, status, requestedOrderNo, remote = true) {
|
|
44
|
+
const response = object(data);
|
|
45
|
+
const follow = { ...base(), order_no: requestedOrderNo };
|
|
46
|
+
if (status === 403 && (response?.required_scope === 'services:read' || response?.requiredScope === 'services:read')) {
|
|
47
|
+
follow.permission_required = 'services:read';
|
|
48
|
+
follow.message = remote
|
|
49
|
+
? 'Service readiness could not be read. If services:read is missing, ask the customer to approve it by re-authorizing this existing connection. Do not grant it automatically or submit another order.'
|
|
50
|
+
: 'Service readiness could not be read. Ask the owner to approve services:read for this management API key in the VPSnet panel, then inspect the same order. Do not change permissions automatically or submit another order.';
|
|
51
|
+
}
|
|
52
|
+
else if (status >= 200 && status < 300 && response?.success === true) {
|
|
53
|
+
const service = object(response.service);
|
|
54
|
+
const extra = object(service?.extra);
|
|
55
|
+
const processValue = service && 'stateProcess' in service ? service.stateProcess : service?.stateProccess;
|
|
56
|
+
const process = object(processValue);
|
|
57
|
+
const state = service?.state;
|
|
58
|
+
const sameOrder = service?.orderNo === requestedOrderNo;
|
|
59
|
+
if (service && sameOrder && typeof state === 'string') {
|
|
60
|
+
follow.next_check = { tool: 'get_service', arguments: { orderNo: requestedOrderNo } };
|
|
61
|
+
follow.guest_agent_available = typeof extra?.guestAgentAvailable === 'boolean' ? extra.guestAgentAvailable : null;
|
|
62
|
+
const waiting = extra?.pendingActivation === true || extra?.awaitingCapacity === true
|
|
63
|
+
|| extra?.awaitingStock === true || service.finalizing === true || process !== undefined;
|
|
64
|
+
if (state === 'create_failed') {
|
|
65
|
+
follow.provisioning_state = 'failed';
|
|
66
|
+
follow.next_check = null;
|
|
67
|
+
follow.message = 'The service reports create_failed. Preserve its createFailure reason and original order; report the failure and any operator attention. Do not automatically order, pay, retry provisioning or restart it.';
|
|
68
|
+
}
|
|
69
|
+
else if (process?.attention === true || extra?.operatorWaiting === true
|
|
70
|
+
|| (extra?.pendingActivation === true && extra.pendingActivationRefresh === false)) {
|
|
71
|
+
follow.provisioning_state = 'needs_attention';
|
|
72
|
+
follow.next_check = null;
|
|
73
|
+
follow.message = 'This service reports operator attention. Report the original order number and observed reason; do not blindly retry, reorder, pay again or restart it.';
|
|
74
|
+
}
|
|
75
|
+
else if (waiting || ['creating', 'starting', 'restarting'].includes(state)) {
|
|
76
|
+
follow.provisioning_state = 'pending';
|
|
77
|
+
follow.poll_after_seconds = 10;
|
|
78
|
+
follow.message = pendingMessage;
|
|
79
|
+
}
|
|
80
|
+
else if (state === 'running' && extra?.pendingActivation === false
|
|
81
|
+
&& service.finalizing === false && processValue === null) {
|
|
82
|
+
follow.provisioning_state = 'complete';
|
|
83
|
+
follow.server_running = true;
|
|
84
|
+
follow.next_check = null;
|
|
85
|
+
follow.message = 'The service reports a running VM with provisioning and finalization clear. Guest-agent availability is separate and advisory; verify the required guest capability before using it. Application health, endpoint reachability and Windows license activation are not proved by this result.';
|
|
86
|
+
}
|
|
87
|
+
else if (['stopped', 'suspended'].includes(state)) {
|
|
88
|
+
follow.provisioning_state = 'not_running';
|
|
89
|
+
follow.server_running = false;
|
|
90
|
+
follow.next_check = null;
|
|
91
|
+
follow.message = 'The service is not running. Inspect the observed state and the original order before proposing a change; do not automatically reorder or restart.';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { ...(response ?? { success: false, error: 'invalid_service_response' }), follow_up: follow };
|
|
96
|
+
}
|
|
97
|
+
/** Preserve the financial receipt, then explicitly hand off to its service. */
|
|
98
|
+
export function purchaseReadiness(data, status, key, canReadServices, receiptRecovery = true) {
|
|
99
|
+
const response = object(data) ?? { success: false, error: 'invalid_purchase_response' };
|
|
100
|
+
// Domain receipts belong to the registrar workflow, not service provisioning.
|
|
101
|
+
if (typeof response.domain_id === 'number' || object(response.domain) || typeof response.domain === 'string')
|
|
102
|
+
return response;
|
|
103
|
+
const follow = { ...base(), idempotency_key: key, purchase_state: 'unknown' };
|
|
104
|
+
const parsedOrder = orderNo.safeParse(response.orderno);
|
|
105
|
+
if (status >= 200 && status < 300 && response.success === true && parsedOrder.success) {
|
|
106
|
+
follow.purchase_state = 'accepted';
|
|
107
|
+
follow.order_no = parsedOrder.data;
|
|
108
|
+
follow.next_check = { tool: 'get_service', arguments: { orderNo: parsedOrder.data } };
|
|
109
|
+
if (!canReadServices) {
|
|
110
|
+
follow.permission_required = 'services:read';
|
|
111
|
+
follow.message = 'The financial request succeeded; provisioning is not verified. Ask the customer to approve services:read by re-authorizing this existing connection, then inspect this same order. Never grant permissions automatically, create another connection/order, pay again or restart the server.';
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
follow.message = 'The financial request succeeded; this receipt is not server readiness. Call get_service with this order number now, then follow its state checks. Retain the original key and order; never pay or order again because provisioning is slow.';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
follow.next_check = { tool: 'get_purchase_result', arguments: { idempotencyKey: key } };
|
|
119
|
+
follow.poll_after_seconds = 10;
|
|
120
|
+
if (status === 202 && response.state === 'pending' && response.idempotencyKey === key)
|
|
121
|
+
follow.purchase_state = 'pending';
|
|
122
|
+
follow.message = 'The financial outcome is pending or unknown. Inspect get_purchase_result with this original key; do not submit another confirmation, order or key. Check at most 6 times at about 10-second intervals, then report the unresolved outcome and retain the key. A timeout is not proof of failure; no background notification is scheduled.';
|
|
123
|
+
if (!receiptRecovery) {
|
|
124
|
+
follow.next_check = null;
|
|
125
|
+
follow.poll_after_seconds = null;
|
|
126
|
+
follow.message = 'The financial outcome is unresolved. Inspect this original attempt in the VPSnet panel; local API-key ordering has no durable purchase-result tool. Retain the key and never submit another confirmation, order or key to retry an uncertain purchase.';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { ...response, follow_up: follow };
|
|
130
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** One safety contract across transports; directory clients never collect a password. */
|
|
2
|
+
export function reinstallDescription(directory) {
|
|
3
|
+
const safety = "Reinstall OS on a VPS or qualified dedicated server. WARNING: destroys existing data; confirm with the user before reinstalling. Cloud VPS and Firecracker VPS: reinstall also deletes existing snapshots, so they cannot provide rollback after reinstall. Read get_os_options.snapshotDeletion first; when snapshots exist, set confirmSnapshotDelete to true only after the user explicitly confirms their deletion. A pending snapshot action blocks reinstall until it finishes. Container VPS has no snapshots. Dedicated servers erase every disk and have no snapshot rollback; get_os_options reports dedicated=true and confirmRequired=true. Pass confirmReinstall=true only after the user explicitly approves reinstalling that exact dedicated server and losing all disk data.";
|
|
4
|
+
const credentials = directory
|
|
5
|
+
? "Use an account public sshKey ID from list_ssh_keys. If omitted, a dedicated server reuses its installed key when available; otherwise VPSnet generates a password and delivers it through the customer's existing VPSnet access flow, not this tool. Do not ask for or pass passwords through this connector; use the VPSnet panel to set a chosen password."
|
|
6
|
+
: "Use an account public sshKey ID from list_ssh_keys or an optional rootPassword, never both. If neither is supplied, a dedicated server reuses its installed key when available; otherwise VPSnet generates a password. Password rules: 6-40 chars, alphanumeric, uppercase + lowercase + digit.";
|
|
7
|
+
return `${safety} ${credentials} Dedicated reinstall returns a reinstall job ID. VPS reinstall returns a noty UUID for acceptance; follow get_service_history for completion. Acceptance is not completion. OAuth requires services:reinstall; API keys require full access.`;
|
|
8
|
+
}
|
package/build/tool-result.js
CHANGED
|
@@ -7,7 +7,18 @@ function failurePayload(value) {
|
|
|
7
7
|
const hasError = error === true
|
|
8
8
|
|| (typeof error === "string" && error.trim().length > 0)
|
|
9
9
|
|| (error !== null && typeof error === "object" && Object.keys(error).length > 0);
|
|
10
|
-
|
|
10
|
+
// A validated asynchronous purchase receipt says success=false while its
|
|
11
|
+
// financial outcome is pending. Keep that resumable read distinct from a
|
|
12
|
+
// failed payment. Explicit errors and HTTP failures still take precedence.
|
|
13
|
+
const follow = payload.follow_up;
|
|
14
|
+
const next = follow?.next_check;
|
|
15
|
+
const purchasePending = payload.state === 'pending'
|
|
16
|
+
&& typeof payload.idempotencyKey === 'string'
|
|
17
|
+
&& follow?.purchase_state === 'pending'
|
|
18
|
+
&& follow.idempotency_key === payload.idempotencyKey
|
|
19
|
+
&& next?.tool === 'get_purchase_result'
|
|
20
|
+
&& next.arguments?.idempotencyKey === payload.idempotencyKey;
|
|
21
|
+
return (payload.success === false && !purchasePending)
|
|
11
22
|
|| hasError
|
|
12
23
|
|| (typeof payload.http_status === "number" && payload.http_status >= 400)
|
|
13
24
|
|| payload.state === "error";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vpsnet-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for managing VPSnet.com services, applications, DNS, domains, billing, and server lifecycle operations",
|
|
6
6
|
"bin": {
|
|
@@ -23,14 +23,18 @@
|
|
|
23
23
|
"build/auth-failure.js",
|
|
24
24
|
"build/certificate-contract.js",
|
|
25
25
|
"build/custom-project-contract.js",
|
|
26
|
+
"build/dns-zone-contract.js",
|
|
26
27
|
"build/file-restore-contract.js",
|
|
27
28
|
"build/index.js",
|
|
29
|
+
"build/service-reinstall-contract.js",
|
|
28
30
|
"build/service-rescue-contract.js",
|
|
31
|
+
"build/service-readiness.js",
|
|
29
32
|
"build/temp-vm-contract.js",
|
|
30
33
|
"build/api-path.js",
|
|
31
34
|
"build/tool-result.js",
|
|
32
35
|
"build/tool-schema.js",
|
|
33
36
|
"build/networking-tools.js",
|
|
37
|
+
"build/service-files.js",
|
|
34
38
|
"build/remote.js",
|
|
35
39
|
"build/http-server.js",
|
|
36
40
|
"build/request-context.js",
|
|
@@ -38,6 +42,7 @@
|
|
|
38
42
|
"build/scope-catalog.js",
|
|
39
43
|
"build/remote-auth-error.js",
|
|
40
44
|
"build/remote-tools.js",
|
|
45
|
+
"build/read-only-context.js",
|
|
41
46
|
"build/update-info.js",
|
|
42
47
|
"scripts/update-clients.json",
|
|
43
48
|
"build/remote-orders.js"
|