vpsnet-mcp 2.3.15 → 2.3.16
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 +2 -2
- package/build/index.js +14 -7
- package/build/read-only-context.js +111 -0
- package/build/remote-orders.js +11 -5
- package/build/remote-policy.js +139 -2
- package/build/remote-tools.js +1 -0
- package/build/service-readiness.js +130 -0
- package/build/tool-result.js +12 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -671,7 +671,7 @@ everything else.
|
|
|
671
671
|
| Tool | Description | Scope | Paid |
|
|
672
672
|
|------|-------------|-------|------|
|
|
673
673
|
| `list_services` | List all active VPS services | `services:read` | free |
|
|
674
|
-
| `get_service` |
|
|
674
|
+
| `get_service` | Read service details and the next readiness check; keep provisioning, guest access and application health separate | `services:read` | free |
|
|
675
675
|
| `get_service_graphs` | Get one graph series: metric (cpu, ram, ssd, net, io, …) and period (5m … 1y) | `services:read` | free |
|
|
676
676
|
| `get_event` | Read an async action's state by its `event` id or `noty` UUID | `services:read` or `account:read` | free |
|
|
677
677
|
| `wait_for_event` | Poll an async action until completed or error (or a timeout) | `services:read` or `account:read` | free |
|
|
@@ -1005,7 +1005,7 @@ enabled.
|
|
|
1005
1005
|
### Guest Agent
|
|
1006
1006
|
| Tool | Description | Scope | Paid |
|
|
1007
1007
|
|------|-------------|-------|------|
|
|
1008
|
-
| `get_guest_agent_status` |
|
|
1008
|
+
| `get_guest_agent_status` | Read guest-agent availability on Cloud VPS or Firecracker; this does not establish application readiness | `services:read` | free |
|
|
1009
1009
|
|
|
1010
1010
|
### History
|
|
1011
1011
|
| Tool | Description | Scope | Paid |
|
package/build/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { installToolInputSchemas } from "./tool-schema.js";
|
|
3
3
|
import { registerOrderTools } from "./remote-orders.js";
|
|
4
4
|
import { registerRemoteTools } from "./remote-tools.js";
|
|
5
|
+
import { registerReadOnlyContext } from "./read-only-context.js";
|
|
6
|
+
import { purchaseReadiness, serviceReadiness, serviceResultOutput } from "./service-readiness.js";
|
|
5
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
9
|
import { pathToFileURL } from "node:url";
|
|
@@ -57,7 +59,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
57
59
|
"Resources is an array of numeric resource value IDs from get_order_options, e.g. [901, 907].",
|
|
58
60
|
"rootPassword rules: 6-40 chars, alphanumeric only, MUST contain uppercase + lowercase + digit. Example: 'MyPass123'.",
|
|
59
61
|
"sshKey and rootPassword are mutually exclusive — provide one or the other (or neither for auto-generated password).",
|
|
60
|
-
"
|
|
62
|
+
"A paid receipt is not a running server. Read get_service for the original order and follow follow_up: pending activation, finalization and capacity waits require polling the same order. About 10 seconds is a suggested polling interval, never a boot ETA. Check at most 6 times before a progress update; if still pending, retain the key and order number for a later check. Do not reorder, repay, restart or promise a background callback. A running VM, guest access and a healthy application are separate outcomes; KVM and Windows setup may take longer and must be observed.",
|
|
61
63
|
"deploy_ssh_key is also async — wait 15-30 seconds after deploying a key before attempting SSH to any VPS (not just new ones).",
|
|
62
64
|
"",
|
|
63
65
|
"## Deployment capabilities (unordered)",
|
|
@@ -347,13 +349,15 @@ export function createVpsnetServer(options = {}) {
|
|
|
347
349
|
return { content: [{ type: "text", text: formatJson(data) }] };
|
|
348
350
|
});
|
|
349
351
|
server.registerTool("get_service", {
|
|
350
|
-
description: "Get detailed info for
|
|
352
|
+
description: "Get detailed info and follow_up readiness guidance for the original service order. Payment, VM provisioning, guest-agent availability and application health are separate. Pending activation, finalizing or active stateProcess means keep polling this same order; about 10 seconds is an interval, not an ETA. Check at most 6 times before a progress update, then retain the order for the next check. stateProcess.waitingReason=public_ip_inventory resumes automatically; never reorder, pay again or manually restart. stateProcess.attention=true requires operator attention. The legacy stateProccess alias carries the same process data. Unknown guest-agent status alone does not prove the VM is down. Resource-usage rows with available=false contain compatibility placeholders, not measurements. Requires services:read when called with an API key.",
|
|
351
353
|
inputSchema: {
|
|
352
354
|
orderNo: serviceOrderNoSchema,
|
|
353
355
|
},
|
|
356
|
+
outputSchema: serviceResultOutput,
|
|
354
357
|
}, async ({ orderNo }) => {
|
|
355
|
-
const { data } = await apiRequest("GET", `/account/services/${encodeURIComponent(orderNo)}`);
|
|
356
|
-
|
|
358
|
+
const { status, data } = await apiRequest("GET", `/account/services/${encodeURIComponent(orderNo)}`);
|
|
359
|
+
const output = serviceReadiness(data, status, orderNo, options.remoteScopes !== undefined);
|
|
360
|
+
return { content: [{ type: "text", text: formatJson(output) }], structuredContent: output };
|
|
357
361
|
});
|
|
358
362
|
server.registerTool("get_service_graphs", {
|
|
359
363
|
description: "Get one performance graph series for a service: pick a metric and a period. fc_block, fc_net_native and fc_pressure exist only for Firecracker VPS. fields narrows io/net/fc_* metrics to some series. A wrong metric, period or field answers 422 with invalidParameter and allowedValues. Requires services:read when called with an API key.",
|
|
@@ -2020,6 +2024,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
2020
2024
|
"Payment object for balance: { payment: 1, successUrl: '', cancelUrl: '' }.",
|
|
2021
2025
|
"First call returns confirmationRequired and an exact gross EUR quote without charging. Show the amount and billing period, obtain explicit approval, then call again with the unchanged order configuration and idempotencyKey, returned quoteToken, expected_total_eur and acknowledge_charge=true. The API key must have paid scope/caps enabled. Never replace the key after an uncertain payment; inspect that original attempt in the VPSnet panel. Quotes are retained in this MCP process; after a restart inspect the original attempt in the panel rather than submitting another order.",
|
|
2022
2026
|
"Payment acceptance is not server readiness. Poll get_service for the returned order number. stateProcess.waitingReason=public_ip_inventory resumes the same paid CREATE automatically when inventory returns; do not place or pay for another order. stateProcess.attention=true needs operator attention.",
|
|
2027
|
+
"Follow follow_up for the same service. Poll about every 10 seconds, never treating that interval as a boot ETA; after at most 6 checks report pending progress and retain the original key/order. Do not automatically restart or promise a completion callback. Running VM, usable guest access and application health are separate checks.",
|
|
2023
2028
|
"Resources: array of numeric resource value IDs from get_order_options, e.g. [901, 907].",
|
|
2024
2029
|
"rootPassword: 6-40 chars, alphanumeric, must contain uppercase + lowercase + digit. Example: 'MyPass123'.",
|
|
2025
2030
|
"sshKey and rootPassword are mutually exclusive — provide one or the other.",
|
|
@@ -2103,7 +2108,8 @@ export function createVpsnetServer(options = {}) {
|
|
|
2103
2108
|
confirmationRequired: true, idempotencyKey,
|
|
2104
2109
|
message: "The original quote is no longer valid. The API refused confirmation before charging. Inspect this original attempt in the VPSnet panel and obtain approval of any changed price or configuration; do not retry an uncertain purchase with a new key." }) }] };
|
|
2105
2110
|
}
|
|
2106
|
-
|
|
2111
|
+
const output = purchaseReadiness(data, status, idempotencyKey, true, false);
|
|
2112
|
+
return { ...(status >= 400 ? { isError: true } : {}), content: [{ type: "text", text: formatJson(output) }], structuredContent: output };
|
|
2107
2113
|
}
|
|
2108
2114
|
catch {
|
|
2109
2115
|
return refusal("purchaseOutcomeUnknown", "The confirmation outcome is unknown. Inspect this original attempt in the VPSnet panel. Do not submit another order or use a new key.");
|
|
@@ -3840,7 +3846,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
3840
3846
|
};
|
|
3841
3847
|
});
|
|
3842
3848
|
server.registerTool("get_guest_agent_status", {
|
|
3843
|
-
description: "
|
|
3849
|
+
description: "Read guest-agent availability for Cloud VPS (KVM/VDS) or Firecracker. KVM probes its QEMU agent; Firecracker reads worker-reported health. This is separate from VM provisioning and application health. Unknown is advisory, not proof the server failed; unavailable gates only operations that need the agent. Delegated Linux tools do not provide Windows PowerShell or RDP verification. Requires services:read when called with an API key.",
|
|
3844
3850
|
inputSchema: {
|
|
3845
3851
|
orderNo: serviceOrderNoSchema,
|
|
3846
3852
|
},
|
|
@@ -4639,7 +4645,8 @@ export function createVpsnetServer(options = {}) {
|
|
|
4639
4645
|
if (options.remoteScopes)
|
|
4640
4646
|
registerRemoteTools(server);
|
|
4641
4647
|
if (options.remoteProfile === "orders")
|
|
4642
|
-
registerOrderTools(server);
|
|
4648
|
+
registerOrderTools(server, options.remoteScopes);
|
|
4649
|
+
registerReadOnlyContext(server);
|
|
4643
4650
|
return server;
|
|
4644
4651
|
}
|
|
4645
4652
|
// --- Start server ---
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { McpError, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { scopeCatalog } from "./scope-catalog.js";
|
|
4
|
+
// Packaged, static guidance only: no filesystem paths, customer data or API calls.
|
|
5
|
+
const guides = [
|
|
6
|
+
{
|
|
7
|
+
name: "connection-access",
|
|
8
|
+
title: "VPSnet connection and permissions",
|
|
9
|
+
description: "Understand discovery, permissions and safe read-only use.",
|
|
10
|
+
mimeType: "text/markdown",
|
|
11
|
+
text: [
|
|
12
|
+
"# Connection and permissions",
|
|
13
|
+
"Discover tools with tools/list; their descriptions, input schemas and annotations describe the current connection. Discovery is not permission to execute every listed tool.",
|
|
14
|
+
"Hosted access uses OAuth. Local stdio access uses a management API key. Never paste credentials into prompts or resources. Use the client's supported authorization flow.",
|
|
15
|
+
"Use get_connected_account when available to inspect the current grant. Resource visibility and allowed services are enforced by the backend. Missing permission requires the user's explicit authorization; never auto-grant or silently broaden access.",
|
|
16
|
+
"These resources contain static guidance, not current account data. Prompts prepare read-only workflows and never execute tools themselves. A client may choose whether to expose resources or prompts in its interface.",
|
|
17
|
+
"Honor the user's existing explicit authorization. Explain the target and effect and obtain approval when an action extends that authorization, destroys unrelated data or adds unapproved charges or exposure. A granted OAuth scope alone does not authorize expanding the task. Functions invocation can incur charges; inspecting an existing invocation does not require starting another one.",
|
|
18
|
+
].join("\n\n"),
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "async-operations",
|
|
22
|
+
title: "VPSnet asynchronous operation progress",
|
|
23
|
+
description: "Follow an existing operation without repeating a change.",
|
|
24
|
+
mimeType: "text/markdown",
|
|
25
|
+
text: [
|
|
26
|
+
"# Following existing operations",
|
|
27
|
+
"Keep the original service identifier, operation identifier and idempotency key. An accepted receipt is not proof that provisioning, guest boot or application startup has finished. Do not repeat a change or create another order because a response is slow.",
|
|
28
|
+
"For networking, follow the read tools named in the receipt. Poll get_networking_operation for network/port operations and get_network_appliance_operation for appliance operations; other operation stores have their own documented read tools. in_progress=true and reason_kind=waiting indicate progress, including waiting_dependency or a control-plane-not-yet-answered reason. They are not an MCP tool failure.",
|
|
29
|
+
"Respect next_check_seconds when provided. If absent, suggest a 10-second interval as polling guidance, not an ETA. Bound a monitoring pass to six reads, then return a pending update containing the identifier and next read needed so the user can resume. Never promise a callback or push notification from the stateless HTTP relay.",
|
|
30
|
+
"When in_progress=false, inspect the terminal state and reason_kind. Report failure or cancellation honestly and do not retry automatically. HTTP or authorization errors remain errors even while an operation is pending.",
|
|
31
|
+
"Read get_service for service state, get_event for an existing service event, and get_guest_agent_status when available for guest-agent evidence. VM provisioned/running, OS or guest agent ready, and application healthy are separate observations; absence of an agent does not prove the OS failed. Use application health reads for application readiness. Firecracker, KVM and Windows can take different amounts of time; no fixed completion time is guaranteed.",
|
|
32
|
+
].join("\n\n"),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "read-only-workflows",
|
|
36
|
+
title: "VPSnet read-only diagnostic workflows",
|
|
37
|
+
description: "Choose existing read tools and report evidence with its limits.",
|
|
38
|
+
mimeType: "text/markdown",
|
|
39
|
+
text: [
|
|
40
|
+
"# Read-only diagnostics",
|
|
41
|
+
"Start with tools/list and check the current input schema. Use only tools whose annotations declare readOnlyHint=true for a read-only diagnostic pass. Some tools named inspect, discover or browse can enqueue durable work and must not be assumed read-only.",
|
|
42
|
+
"Service: list_services, then get_service, get_service_history and get_guest_agent_status where available and permitted. Identify the exact service before interpreting its state.",
|
|
43
|
+
"Networking: read get_networking_capabilities and the relevant network, topology, port or existing operation. A successful control-plane operation alone does not prove guest connectivity.",
|
|
44
|
+
"Applications: list_service_applications, then read get_application_installation, get_application_health and bounded events or logs for the selected installation. Redact credentials and personal data from quoted logs. Do not run install, restart, repair or reconcile as part of diagnosis.",
|
|
45
|
+
"Functions: use permitted existing invocation/history reads. list_functions and get_function may reconcile saved trace state; their GET method and functions:read scope do not prove they are side-effect free. Do not use those tools for a strictly side-effect-free review unless the user already authorized that reconciliation. Check current annotations and report limitations. Do not invoke a function to check it; execution is a separate user-authorized action with possible usage charges.",
|
|
46
|
+
"Report observed facts, unresolved questions and the smallest proposed next step. A permission denial, missing telemetry or pending operation is not proof that a service is broken.",
|
|
47
|
+
].join("\n\n"),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "oauth-scopes",
|
|
51
|
+
title: "VPSnet OAuth scope catalog",
|
|
52
|
+
description: "Canonical nonlegacy scope names, descriptions, implications and billing categories; not the connection's grants.",
|
|
53
|
+
mimeType: "application/json",
|
|
54
|
+
text: JSON.stringify({
|
|
55
|
+
kind: "scope_catalog",
|
|
56
|
+
grants: false,
|
|
57
|
+
note: "Static supported-scope documentation, not granted permissions. Discovery profile and consent determine availability. Inspect get_connected_account when available for this connection's actual grant. Legacy scopes are omitted.",
|
|
58
|
+
scopes: Object.entries(scopeCatalog).filter(([, entry]) => !entry.legacy)
|
|
59
|
+
.map(([name, entry]) => ({ name, ...entry })),
|
|
60
|
+
}, null, 2),
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
const workflows = [
|
|
64
|
+
["review-service", "Review service readiness", "Review one existing service using read-only evidence.", "Read list_services if an identifier is needed, then get_service and permitted history/guest-agent reads. Separate provisioning, OS/guest-agent readiness and application readiness. Do not restart, reinstall or create another service."],
|
|
65
|
+
["review-network", "Review network progress", "Inspect existing networking state and operation progress.", "Read get_networking_capabilities and the selected network/port or existing operation using the receipt's follow-up read. Treat in_progress=true with reason_kind=waiting as progress; respect next_check_seconds and return a resumable pending update after bounded polling. Do not attach, retry, reconfigure or create resources."],
|
|
66
|
+
["review-application", "Review application health", "Inspect an existing application's health without changing it.", "Read list_service_applications if needed, then the selected installation, health and bounded events/logs. Redact secrets from logs. Report container/application evidence separately from VM readiness. Do not install, restart, repair, reconcile or change access."],
|
|
67
|
+
["review-function", "Review function history", "Inspect prior function invocations without executing a function.", "Read permitted existing invocation/history records and distinguish observed outcomes from unknown configuration. list_functions and get_function may reconcile saved trace state; do not use them for a strictly side-effect-free review unless the user already authorized that reconciliation. Check current annotations and report limitations. Do not invoke, create, update or delete a function, and do not expose secrets."],
|
|
68
|
+
];
|
|
69
|
+
export function registerReadOnlyContext(server) {
|
|
70
|
+
for (const guide of guides) {
|
|
71
|
+
const uri = `vpsnet://docs/${guide.name}`;
|
|
72
|
+
server.registerResource(guide.name, uri, {
|
|
73
|
+
title: guide.title, description: guide.description, mimeType: guide.mimeType,
|
|
74
|
+
}, async () => ({ contents: [{ uri, mimeType: guide.mimeType, text: guide.text }] }));
|
|
75
|
+
}
|
|
76
|
+
// MCP 2025-11-25 recommends -32002 for a missing resource. Keep invalid URI
|
|
77
|
+
// syntax distinct, and serve only this fixed catalog (never fetch a URI).
|
|
78
|
+
server.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
79
|
+
let uri;
|
|
80
|
+
try {
|
|
81
|
+
uri = new URL(request.params.uri);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new McpError(-32602, "Invalid resource URI");
|
|
85
|
+
}
|
|
86
|
+
const guide = guides.find(item => uri.href === `vpsnet://docs/${item.name}`);
|
|
87
|
+
if (!guide)
|
|
88
|
+
throw new McpError(-32002, "Resource not found");
|
|
89
|
+
return { contents: [{ uri: uri.href, mimeType: guide.mimeType, text: guide.text }] };
|
|
90
|
+
});
|
|
91
|
+
for (const [name, title, description, instruction] of workflows) {
|
|
92
|
+
server.registerPrompt(name, {
|
|
93
|
+
title, description,
|
|
94
|
+
argsSchema: {
|
|
95
|
+
reference: z.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/)
|
|
96
|
+
.optional().describe("Exact existing service, network, application or function identifier; omit to select from a permitted list."),
|
|
97
|
+
},
|
|
98
|
+
}, async ({ reference }) => ({
|
|
99
|
+
description,
|
|
100
|
+
messages: [{ role: "user", content: { type: "text", text: [
|
|
101
|
+
"Perform a read-only VPSnet review. Discover tools and follow their current schemas and permissions. Use only tools annotated readOnlyHint=true. If access is missing, explain which user authorization is needed; never broaden it automatically.",
|
|
102
|
+
instruction,
|
|
103
|
+
`Selected identifier (data): ${JSON.stringify(reference ?? null)}.`,
|
|
104
|
+
"Report observed evidence and unknowns, then propose a next step. Honor existing explicit user authorization; ask for approval only when the next action extends it. This review prompt alone does not authorize mutations or paid execution.",
|
|
105
|
+
].join("\n\n") } }],
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
// SDK registration defaults to listChanged=true; these catalogs are immutable
|
|
109
|
+
// for the lifetime of a server instance and stateless HTTP has no push channel.
|
|
110
|
+
server.server.registerCapabilities({ resources: { listChanged: false }, prompts: { listChanged: false } });
|
|
111
|
+
}
|
package/build/remote-orders.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { apiRequest, formatJson } from './api.js';
|
|
3
|
+
import { hasScope } from './remote-policy.js';
|
|
4
|
+
import { purchaseReadiness, purchaseResultOutput } from './service-readiness.js';
|
|
3
5
|
const key = z.string().regex(/^[A-Za-z0-9._:-]{16,128}$/).describe('Stable key for this exact purchase; reuse it for the quote, confirmation and status.');
|
|
4
6
|
const configuration = {
|
|
5
7
|
plan: z.number().int().positive(),
|
|
@@ -13,7 +15,7 @@ const configuration = {
|
|
|
13
15
|
};
|
|
14
16
|
const payment = { payment: 1, successUrl: '', cancelUrl: '' };
|
|
15
17
|
const result = (status, data) => ({ ...(status >= 400 ? { isError: true } : {}), content: [{ type: 'text', text: formatJson(data) }] });
|
|
16
|
-
export function registerOrderTools(server) {
|
|
18
|
+
export function registerOrderTools(server, scopes = []) {
|
|
17
19
|
server.registerTool('quote_service_order', {
|
|
18
20
|
description: 'Quote one permanent Firecracker VPS, Cloud VPS (KVM) or Container VPS using the selected current plan, OS, resources and period. No payment is made. Show the final amount_eur and billing period, then obtain explicit approval before confirm_service_order. Functions is a separate usage-billed product.',
|
|
19
21
|
inputSchema: configuration,
|
|
@@ -23,23 +25,27 @@ export function registerOrderTools(server) {
|
|
|
23
25
|
return result(status, data);
|
|
24
26
|
});
|
|
25
27
|
server.registerTool('confirm_service_order', {
|
|
26
|
-
description: 'Purchase the exact previously quoted server from VPSnet balance after explicit customer approval of its final EUR total and billing period. Supply the unchanged quote configuration, token and key. Never automatically create a new key after an uncertain result: inspect get_purchase_result. A receipt is not a ready server. Poll get_purchase_result for the
|
|
28
|
+
description: 'Purchase the exact previously quoted server from VPSnet balance after explicit customer approval of its final EUR total and billing period. Supply the unchanged quote configuration, token and key. Never automatically create a new key after an uncertain result: inspect get_purchase_result. A receipt is not a ready server. Poll get_purchase_result for the financial outcome; get_service for the returned orderno additionally requires services:read. Follow follow_up for provisioning checks, permission requirements and bounded progress updates. The suggested 10-second polling interval is not an ETA. This connection receives access to its newly ordered server only within the permissions the customer granted.',
|
|
27
29
|
inputSchema: { ...configuration,
|
|
28
30
|
quoteToken: z.string().min(32).max(256),
|
|
29
31
|
expected_total_eur: z.number().finite().nonnegative().describe('Exact final amount_eur from the quote, shown to and approved by the customer.'),
|
|
30
32
|
acknowledge_charge: z.literal(true).describe('The customer explicitly approved this exact balance charge.'),
|
|
31
33
|
},
|
|
34
|
+
outputSchema: purchaseResultOutput,
|
|
32
35
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
33
36
|
}, async (input) => {
|
|
34
37
|
const { status, data } = await apiRequest('POST', '/account/mcp/orders/services/confirm', { ...input, payment }, { 'Idempotency-Key': input.idempotencyKey, 'X-Quote-Token': input.quoteToken });
|
|
35
|
-
|
|
38
|
+
const output = purchaseReadiness(data, status, input.idempotencyKey, hasScope(scopes, 'services:read'));
|
|
39
|
+
return { ...result(status, output), structuredContent: output };
|
|
36
40
|
});
|
|
37
41
|
server.registerTool('get_purchase_result', {
|
|
38
|
-
description: 'Inspect this connection’s existing purchase attempt by its original idempotency key.
|
|
42
|
+
description: 'Inspect this connection’s existing financial purchase attempt by its original idempotency key. This replays a financial receipt, not live provisioning progress. Pending or unknown must not trigger a second order. Follow follow_up: a returned server orderno is checked with get_service; if services:read is missing, re-authorize this existing connection without ordering again. A domain remains pending until its registrar status confirms registration. Do not promise an idle client a completion callback.',
|
|
39
43
|
inputSchema: { idempotencyKey: key },
|
|
44
|
+
outputSchema: purchaseResultOutput,
|
|
40
45
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
41
46
|
}, async ({ idempotencyKey }) => {
|
|
42
47
|
const { status, data } = await apiRequest('GET', `/account/mcp/orders/requests/${encodeURIComponent(idempotencyKey)}`);
|
|
43
|
-
|
|
48
|
+
const output = purchaseReadiness(data, status, idempotencyKey, hasScope(scopes, 'services:read'));
|
|
49
|
+
return { ...result(status, output), structuredContent: output };
|
|
44
50
|
});
|
|
45
51
|
}
|
package/build/remote-policy.js
CHANGED
|
@@ -28,7 +28,7 @@ export function orderInstructions(scopes) {
|
|
|
28
28
|
const products = [scopes.includes('orders:servers') ? 'purchase permanent VPS servers' : '',
|
|
29
29
|
scopes.includes('orders:domains') ? 'register domains' : ''].filter(Boolean);
|
|
30
30
|
const instructions = products.length
|
|
31
|
-
? `This VPSnet connection can ${products.join(' and ')} from the customer’s VPSnet balance within its granted permissions. Quote first, show the final gross EUR amount and period, and confirm only after explicit approval. ${scopes.includes('orders:domains') ? 'Domain registration may be non-refundable. ' : ''}Daily and monthly limits cover this connection across token refresh. Never use a new key to retry an uncertain payment; inspect get_purchase_result. Payment settings and saved-card charges are not exposed.`
|
|
31
|
+
? `This VPSnet connection can ${products.join(' and ')} from the customer’s VPSnet balance within its granted permissions. Quote first, show the final gross EUR amount and period, and confirm only after explicit approval. ${scopes.includes('orders:domains') ? 'Domain registration may be non-refundable. ' : ''}Daily and monthly limits cover this connection across token refresh. Never use a new key to retry an uncertain payment; inspect get_purchase_result. A financial receipt is separate from provisioning: follow follow_up and get_service for the same returned order, requiring services:read. If missing, re-authorize this existing connection without granting it automatically or ordering again. Poll about every 10 seconds as a suggested interval, never an ETA; after at most 6 checks report pending progress and retain the original IDs. Do not restart, reorder or promise an idle AI a callback. A running VM, guest access and application health are separate checks. Payment settings and saved-card charges are not exposed.`
|
|
32
32
|
: 'This VPSnet connection can inspect order plans and existing purchase results within its granted permissions. It cannot submit purchases or change payment settings.';
|
|
33
33
|
return remoteInstructions.replace('This connector does not purchase capacity, change payment settings or run model inference.', instructions);
|
|
34
34
|
}
|
|
@@ -160,15 +160,138 @@ const titleAcronyms = {
|
|
|
160
160
|
function defaultToolTitle(name) {
|
|
161
161
|
return name.split(/[_-]+/).filter(Boolean).map((word, index) => titleAcronyms[word.toLowerCase()] ?? (index === 0 ? word[0].toUpperCase() + word.slice(1) : word)).join(' ');
|
|
162
162
|
}
|
|
163
|
+
// Explicit legacy stdio classifications, reviewed against handlers and backend effects.
|
|
164
|
+
// This only fills missing metadata; remote policy supplies every hint and remains unchanged.
|
|
165
|
+
// POST calculate_plan_change only computes a price; POST quotes persist an intent.
|
|
166
|
+
const legacyReadOnlyTools = new Set([
|
|
167
|
+
'get_account',
|
|
168
|
+
'get_profile',
|
|
169
|
+
'list_services',
|
|
170
|
+
'get_service',
|
|
171
|
+
'get_service_graphs',
|
|
172
|
+
'get_event',
|
|
173
|
+
'wait_for_event',
|
|
174
|
+
'get_service_history',
|
|
175
|
+
'get_rdns',
|
|
176
|
+
'get_title',
|
|
177
|
+
'get_os_options',
|
|
178
|
+
'get_plan_options',
|
|
179
|
+
'get_plan_resources',
|
|
180
|
+
'calculate_plan_change',
|
|
181
|
+
'get_period_options',
|
|
182
|
+
'get_order_plans',
|
|
183
|
+
'get_order_options',
|
|
184
|
+
'get_backup_status',
|
|
185
|
+
'get_backup_history',
|
|
186
|
+
'list_ssh_keys',
|
|
187
|
+
'get_ssh_key',
|
|
188
|
+
'list_api_keys',
|
|
189
|
+
'get_api_key',
|
|
190
|
+
'get_api_key_activity',
|
|
191
|
+
'get_api_key_inference_usage',
|
|
192
|
+
'list_domains',
|
|
193
|
+
'get_domain',
|
|
194
|
+
'list_domain_tlds',
|
|
195
|
+
'list_domain_glue_records',
|
|
196
|
+
'get_domain_parent_ds',
|
|
197
|
+
'check_domain_availability',
|
|
198
|
+
'get_domain_ordering_status',
|
|
199
|
+
'list_domain_contacts',
|
|
200
|
+
'get_registrar_lock',
|
|
201
|
+
'list_service_dns_options',
|
|
202
|
+
'list_firecracker_snapshots',
|
|
203
|
+
'get_restore_status',
|
|
204
|
+
'list_restore_points',
|
|
205
|
+
'get_guest_agent_status',
|
|
206
|
+
'list_function_invocations',
|
|
207
|
+
'get_function_invocation',
|
|
208
|
+
'list_dns_zones',
|
|
209
|
+
'get_dns_zone',
|
|
210
|
+
'get_dns_zone_diagnostics',
|
|
211
|
+
'export_dns_zone',
|
|
212
|
+
'list_dns_templates',
|
|
213
|
+
'get_dnssec',
|
|
214
|
+
'get_dns_service_status',
|
|
215
|
+
'get_dns_zone_history',
|
|
216
|
+
'list_ddns_tokens',
|
|
217
|
+
'list_invoices',
|
|
218
|
+
'get_invoice',
|
|
219
|
+
'list_payments',
|
|
220
|
+
'get_login_history',
|
|
221
|
+
'get_management_history',
|
|
222
|
+
'get_usage_statements',
|
|
223
|
+
'get_pricing',
|
|
224
|
+
'get_system_status',
|
|
225
|
+
'get_faq',
|
|
226
|
+
]);
|
|
227
|
+
const legacyWriteDestructive = new Map([
|
|
228
|
+
// GET also advances a pending rollback, including disk/application reconciliation.
|
|
229
|
+
['list_snapshots', true],
|
|
230
|
+
// GET may create/repair the billing anchor and replace a Function's association.
|
|
231
|
+
['list_functions', false],
|
|
232
|
+
['get_function', false],
|
|
233
|
+
// GET reserves/reconciles a platform hostname and queues DNS/PTR/guest work.
|
|
234
|
+
['get_hostname', true],
|
|
235
|
+
['start_service', false],
|
|
236
|
+
['stop_service', true],
|
|
237
|
+
['restart_service', true],
|
|
238
|
+
['console_service', false],
|
|
239
|
+
['suspend_service', true],
|
|
240
|
+
['resume_service', false],
|
|
241
|
+
['change_hostname', true],
|
|
242
|
+
['reset_hostname', true],
|
|
243
|
+
['change_root_password', true],
|
|
244
|
+
['flush_iptables', true],
|
|
245
|
+
['change_title', true],
|
|
246
|
+
['toggle_ipv6', true],
|
|
247
|
+
['toggle_extra_settings', true],
|
|
248
|
+
['deploy_ssh_key', true],
|
|
249
|
+
['change_plan', true],
|
|
250
|
+
['set_auto_renew', true],
|
|
251
|
+
['renew_service', true],
|
|
252
|
+
['order_service', true],
|
|
253
|
+
['create_backup', true],
|
|
254
|
+
['create_ssh_key', false],
|
|
255
|
+
['delete_ssh_key', true],
|
|
256
|
+
['create_domain_contact', true],
|
|
257
|
+
['update_domain_contact', true],
|
|
258
|
+
['delete_domain_contact', true],
|
|
259
|
+
['quote_domain_register', false],
|
|
260
|
+
['quote_domain_transfer', false],
|
|
261
|
+
['quote_domain_renew', false],
|
|
262
|
+
['quote_domain_restore', false],
|
|
263
|
+
['set_domain_auto_renew', true],
|
|
264
|
+
['create_snapshot', true],
|
|
265
|
+
['rollback_snapshot', true],
|
|
266
|
+
['delete_snapshot', true],
|
|
267
|
+
['create_firecracker_snapshot', false],
|
|
268
|
+
['rollback_firecracker_snapshot', true],
|
|
269
|
+
['delete_firecracker_snapshot', true],
|
|
270
|
+
['reissue_dns_zone_verification', true],
|
|
271
|
+
['create_ddns_token', false],
|
|
272
|
+
['revoke_ddns_token', true],
|
|
273
|
+
]);
|
|
163
274
|
/** Give every transport a discoverable title while retaining authored annotations. */
|
|
164
275
|
export function installToolTitles(server) {
|
|
165
276
|
const original = server.registerTool.bind(server);
|
|
166
277
|
const register = (name, config, handler) => {
|
|
167
278
|
const annotations = (config.annotations ?? {});
|
|
168
279
|
const authored = [annotations.title, config.title].find(title => typeof title === 'string' && title.trim().length > 0);
|
|
280
|
+
const title = authored ?? defaultToolTitle(name);
|
|
281
|
+
const legacyHints = legacyReadOnlyTools.has(name)
|
|
282
|
+
? { readOnlyHint: true, destructiveHint: false }
|
|
283
|
+
: legacyWriteDestructive.has(name)
|
|
284
|
+
? { readOnlyHint: false, destructiveHint: legacyWriteDestructive.get(name) }
|
|
285
|
+
: {};
|
|
286
|
+
const completed = { ...annotations };
|
|
287
|
+
for (const [key, value] of Object.entries({ ...legacyHints, openWorldHint: externalTools.has(name) })) {
|
|
288
|
+
if (completed[key] === undefined)
|
|
289
|
+
completed[key] = value;
|
|
290
|
+
}
|
|
169
291
|
return original(name, {
|
|
170
292
|
...config,
|
|
171
|
-
|
|
293
|
+
title: config.title || title,
|
|
294
|
+
annotations: { ...completed, title },
|
|
172
295
|
}, handler);
|
|
173
296
|
};
|
|
174
297
|
server.registerTool = register;
|
|
@@ -213,6 +336,16 @@ const deduplicatedTools = new Set([
|
|
|
213
336
|
// These read guest or backup state but queue durable operations, so their read scopes do
|
|
214
337
|
// not imply a read-only MCP call.
|
|
215
338
|
const queuedReadTools = new Set(['browse_restore_files', 'inspect_vm', 'read_vm_file', 'get_application_health', 'get_application_logs', 'discover_service_containers']);
|
|
339
|
+
// Intentional hosted metadata correction: these endpoints can change existing state
|
|
340
|
+
// even though their permission is read-scoped (or default-contact creation is additive
|
|
341
|
+
// only when is_default is false). No permission or handler changes accompany these hints.
|
|
342
|
+
const hostedReconciliationEffects = new Map([
|
|
343
|
+
['list_snapshots', true],
|
|
344
|
+
['get_hostname', true],
|
|
345
|
+
['list_functions', false],
|
|
346
|
+
['get_function', false],
|
|
347
|
+
['create_domain_contact', true],
|
|
348
|
+
]);
|
|
216
349
|
// Keep cached aggregate calls valid, but advertise the individually scoped
|
|
217
350
|
// actions to new directory clients. The SDK uses one handler for tools/list;
|
|
218
351
|
// filtering its result leaves the normal tools/call dispatcher intact.
|
|
@@ -266,6 +399,10 @@ export function installRemotePolicy(server, scopes, profile = 'directory') {
|
|
|
266
399
|
annotations.readOnlyHint = true;
|
|
267
400
|
annotations.destructiveHint = false;
|
|
268
401
|
}
|
|
402
|
+
if (hostedReconciliationEffects.has(name)) {
|
|
403
|
+
annotations.readOnlyHint = false;
|
|
404
|
+
annotations.destructiveHint = hostedReconciliationEffects.get(name);
|
|
405
|
+
}
|
|
269
406
|
if (deduplicatedTools.has(name))
|
|
270
407
|
annotations.idempotentHint = true;
|
|
271
408
|
const objectSchema = config.inputSchema instanceof z.ZodObject ? config.inputSchema : undefined;
|
package/build/remote-tools.js
CHANGED
|
@@ -92,6 +92,7 @@ export function registerRemoteTools(server) {
|
|
|
92
92
|
outputSchema: z.object({
|
|
93
93
|
id: z.string().min(1), name: z.string(), scopes: z.array(z.string()), services: z.array(z.string()),
|
|
94
94
|
function_daily_limit_eur: spendingLimit, order_daily_limit_eur: spendingLimit, order_monthly_limit_eur: spendingLimit,
|
|
95
|
+
function_monthly_limit_eur: spendingLimit.describe('Stored monthly Functions limit in EUR when supplied by the backend. Null or omitted means no explicit monthly cap is reported by this grant, not unlimited usage; account limits still apply. Read only: never infer a default or expand permissions.'),
|
|
95
96
|
}).passthrough(),
|
|
96
97
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
97
98
|
}, async () => { const { data } = await apiRequest('GET', '/account/mcp/profile'); return { ...result(data), structuredContent: data }; });
|
|
@@ -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
|
+
}
|
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.
|
|
3
|
+
"version": "2.3.16",
|
|
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": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"build/file-restore-contract.js",
|
|
27
27
|
"build/index.js",
|
|
28
28
|
"build/service-rescue-contract.js",
|
|
29
|
+
"build/service-readiness.js",
|
|
29
30
|
"build/temp-vm-contract.js",
|
|
30
31
|
"build/api-path.js",
|
|
31
32
|
"build/tool-result.js",
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"build/scope-catalog.js",
|
|
39
40
|
"build/remote-auth-error.js",
|
|
40
41
|
"build/remote-tools.js",
|
|
42
|
+
"build/read-only-context.js",
|
|
41
43
|
"build/update-info.js",
|
|
42
44
|
"scripts/update-clients.json",
|
|
43
45
|
"build/remote-orders.js"
|