vpsnet-mcp 2.3.3 → 2.3.5
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 +5 -5
- package/build/http-server.js +5 -1
- package/build/index.js +2 -2
- package/build/remote-auth-error.js +1 -1
- package/build/remote-policy.js +21 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -545,13 +545,13 @@ Follow the [Windsurf MCP documentation](https://docs.windsurf.com/windsurf/mcp).
|
|
|
545
545
|
| Variable | Required | Description |
|
|
546
546
|
|----------|----------|-------------|
|
|
547
547
|
| `VPSNET_API_KEY` | Yes | Your VPSnet.com API key |
|
|
548
|
-
| `VPSNET_API_URL` | No | API base URL (defaults to `https://api.vpsnet.com`); any `https://*.vpsnet.com` host
|
|
548
|
+
| `VPSNET_API_URL` | No | API base URL (defaults to `https://api.vpsnet.com`); any `https://*.vpsnet.com` host configured for your deployment |
|
|
549
549
|
| `VPSNET_AI_URL` | No | Delegated SSH runtime; production uses `https://ai.vpsnet.com`. Non-production requires the approved API origin followed by `/ai-control`. |
|
|
550
550
|
| `VPSNET_AI_API_ORIGIN` | Non-production SSH only | Operator-approved HTTPS VPSnet API origin; must exactly match `VPSNET_API_URL`. Set both runtime variables privately for a non-production deployment. |
|
|
551
551
|
| `VPSNET_API_TIMEOUT_MS` | No | Per-request timeout (default 45000) |
|
|
552
552
|
|
|
553
553
|
Requests go out from the machine that runs this server, not from your browser. A
|
|
554
|
-
|
|
554
|
+
deployment that restricts access by source address can answer a blocked
|
|
555
555
|
request with an HTML `403` page from its web server; the tool result then says
|
|
556
556
|
so (`non_api_response: true`, `http_status: 403`) instead of passing the page on.
|
|
557
557
|
Every error result carries `http_status`; a `429` also carries `retry_after`.
|
|
@@ -906,9 +906,9 @@ short-lived quote token.
|
|
|
906
906
|
Customer ordering is disabled while the on-demand server lifecycle is being
|
|
907
907
|
validated. Current options disclose that automatic backups and snapshots are
|
|
908
908
|
not included and outbound connections to mail-delivery TCP ports 25, 2525,
|
|
909
|
-
465, and 587 are restricted. Existing
|
|
910
|
-
|
|
911
|
-
|
|
909
|
+
465, and 587 are restricted. Existing sessions remain available through
|
|
910
|
+
list/get/delete for cleanup. Fixed-TTL and prepaid fields in the disabled
|
|
911
|
+
compatibility schema are not a launch promise; the flexible duration
|
|
912
912
|
and metered-billing contract must be published consistently before ordering is
|
|
913
913
|
enabled.
|
|
914
914
|
|
package/build/http-server.js
CHANGED
|
@@ -44,8 +44,12 @@ export function createRemoteServer(options) {
|
|
|
44
44
|
|| typeof value.sub !== "string" || typeof value.grant_id !== "string" || typeof value.scope !== "string"
|
|
45
45
|
|| !Array.isArray(value.services) || !value.services.every(x => typeof x === "string"))
|
|
46
46
|
return null;
|
|
47
|
+
const scopes = value.scope.split(" ").filter(x => (allowedScopes.includes(x) || x === "vm:files"));
|
|
48
|
+
// orders_available is the issuer's live client-policy decision, not a client-supplied hint.
|
|
49
|
+
// A permitted client still needs actual order authority before order capabilities are exposed.
|
|
47
50
|
return { accessToken: match[1], subject: value.sub, grantId: value.grant_id,
|
|
48
|
-
expiresAt: value.exp, ordersAvailable: options.ordersEnabled === true && value.orders_available === true
|
|
51
|
+
expiresAt: value.exp, ordersAvailable: options.ordersEnabled === true && value.orders_available === true
|
|
52
|
+
&& orderScopes.some(scope => scopes.includes(scope)), scopes, services: value.services };
|
|
49
53
|
}
|
|
50
54
|
const http = createServer(async (req, res) => {
|
|
51
55
|
// TLS terminates at the supplied reverse proxy, which must preserve Host.
|
package/build/index.js
CHANGED
|
@@ -41,7 +41,7 @@ const EVENT_ID_SCHEMA = z
|
|
|
41
41
|
.describe("Numeric `event` id or `noty` UUID returned by the action");
|
|
42
42
|
export function createVpsnetServer(options = {}) {
|
|
43
43
|
const server = new McpServer({ name: "vpsnet", version: JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version }, {
|
|
44
|
-
instructions: options.remoteScopes ? (options.remoteProfile === "orders" ? orderInstructions : remoteInstructions) : [
|
|
44
|
+
instructions: options.remoteScopes ? (options.remoteProfile === "orders" ? orderInstructions(options.remoteScopes) : remoteInstructions) : [
|
|
45
45
|
"This MCP server controls VPSnet.com services, including VPS service management, DNS zones, domain registration, domain contacts, API keys, billing, and related paid actions.",
|
|
46
46
|
"Use the tool descriptions and API-key scopes to choose the correct surface; do not assume this server is limited to VPS-only operations.",
|
|
47
47
|
"Auth: every request authenticates with the X-API-KEY header (your VPSNet API key). Requests are rate-limited — on HTTP 429, back off and retry after a short delay rather than hammering the endpoint.",
|
|
@@ -179,7 +179,7 @@ export function createVpsnetServer(options = {}) {
|
|
|
179
179
|
.min(1)
|
|
180
180
|
.max(64)
|
|
181
181
|
.regex(/^[A-Za-z0-9-]+$/, "Use the exact service order number returned by list_services")
|
|
182
|
-
.describe("Tenant-owned service order number: 1-64 ASCII letters, digits or hyphens, e.g.
|
|
182
|
+
.describe("Tenant-owned service order number: 1-64 ASCII letters, digits or hyphens, e.g. VP12345 or VP12345-1");
|
|
183
183
|
const positiveIdSchema = z.number().int().positive().max(Number.MAX_SAFE_INTEGER);
|
|
184
184
|
// Same shape OrderPrivateNetworkAdmission::admit() accepts for an order-time network.
|
|
185
185
|
const privateNetworkIdSchema = z
|
|
@@ -2,7 +2,7 @@ export class RemoteAuthError extends Error {
|
|
|
2
2
|
kind;
|
|
3
3
|
scope;
|
|
4
4
|
constructor(kind, scope) {
|
|
5
|
-
super(kind === 'invalid_token' ? 'The connection has expired or was revoked.
|
|
5
|
+
super(kind === 'invalid_token' ? 'The connection has expired or was revoked. Re-authorize this existing VPSnet connection in your AI app.' : `This action requires ${scope || 'an additional permission'}. Ask the customer to approve this permission by re-authorizing this existing VPSnet connection in their AI app.`);
|
|
6
6
|
this.kind = kind;
|
|
7
7
|
this.scope = scope;
|
|
8
8
|
}
|
package/build/remote-policy.js
CHANGED
|
@@ -7,7 +7,7 @@ export const remoteInstructions = [
|
|
|
7
7
|
"Firecracker VPS supports general-purpose and resource-intensive Linux workloads with substantial server resources and low virtualization overhead. MicroVM describes the lightweight design, not a small server limit. Consider it when KVM-specific features are unnecessary; match current plans to resource and availability requirements.",
|
|
8
8
|
"Cloud VPS (KVM) offers Linux and BSD support, automatic Windows Server provisioning, three-copy Ceph NVMe storage on enterprise drives with power-loss protection (PLP), HA restart on another host after host failure, and live migration to another node for planned host upgrades, with migration back when appropriate.",
|
|
9
9
|
"Use the connected VPSnet account and only the services and permissions it granted. Never request a model API key.",
|
|
10
|
-
"Tool discovery describes supported capabilities, not granted authority.
|
|
10
|
+
"Tool discovery describes supported capabilities, not granted authority. Compare the scopes and selected services from get_connected_account with each tool's required scope metadata. Explain missing permissions only when relevant to the user's task. Ask the customer to approve the required permission by re-authorizing this existing VPSnet connection through the AI app's OAuth flow; never work around a denial.",
|
|
11
11
|
"Inspect current capabilities and state before a change. Honor the user's existing authorization; obtain consent when a change expands the task, destroys unrelated data or adds unapproved exposure.",
|
|
12
12
|
"Keep operation IDs, reason codes and idempotency keys. Accepted means pending; read the final state before claiming success.",
|
|
13
13
|
"On disconnect or uncertain response, inspect the existing operation. Never issue a fresh mutation or new idempotency key automatically.",
|
|
@@ -19,7 +19,16 @@ export const remoteInstructions = [
|
|
|
19
19
|
"This connector does not purchase capacity, change payment settings or run model inference. Functions execution uses the customer's VPSnet balance; an AI subscription does not pay those infrastructure charges.",
|
|
20
20
|
].join("\n");
|
|
21
21
|
export const orderScopes = ["orders:read", "orders:servers", "orders:domains"];
|
|
22
|
-
export
|
|
22
|
+
export function orderInstructions(scopes) {
|
|
23
|
+
if (!orderScopes.some(scope => scopes.includes(scope)))
|
|
24
|
+
return remoteInstructions;
|
|
25
|
+
const products = [scopes.includes('orders:servers') ? 'purchase permanent VPS servers' : '',
|
|
26
|
+
scopes.includes('orders:domains') ? 'register domains' : ''].filter(Boolean);
|
|
27
|
+
const instructions = products.length
|
|
28
|
+
? `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.`
|
|
29
|
+
: 'This VPSnet connection can inspect order plans and existing purchase results within its granted permissions. It cannot submit purchases or change payment settings.';
|
|
30
|
+
return remoteInstructions.replace('This connector does not purchase capacity, change payment settings or run model inference.', instructions);
|
|
31
|
+
}
|
|
23
32
|
export const supportedScopes = ["services:read", "services:manage", "applications:read", "applications:manage", "networking:read", "networking:manage", "functions:read", "functions:manage", "functions:invoke", "vm:diagnostics", "vm:files:read", "vm:files:write", "vm:exec", "services:reinstall", "services:restore", "services:rescue", "dns:read", "dns:write", "dnssec:manage", "domains:read", "domains:manage", "certificates:read", "certificates:manage", "ssh_keys:read", "ssh_keys:manage"];
|
|
24
33
|
const readServices = ["get_service_rescue", "get_guest_agent_status", "get_plan_options", "get_plan_resources", "calculate_plan_change", "get_period_options", "list_restore_points", "list_restore_file_points", "browse_restore_files", "get_restore_file_browse", "list_services", "get_service", "get_service_graphs", "get_service_history", "get_hostname", "get_title", "get_rdns", "get_os_options", "get_backup_status", "get_backup_history", "list_snapshots", "list_firecracker_snapshots", "get_firecracker_backup_status", "get_firecracker_backup_history"];
|
|
25
34
|
const manageServices = ["suspend_service", "resume_service", "flush_iptables", "toggle_ipv6", "toggle_extra_settings", "reinstall_os", "rollback_snapshot", "delete_snapshot", "rollback_firecracker_snapshot", "delete_firecracker_snapshot", "start_service", "stop_service", "restart_service", "change_hostname", "reset_hostname", "change_title", "change_rdns", "clear_rdns"];
|
|
@@ -154,13 +163,16 @@ const deduplicatedTools = new Set([
|
|
|
154
163
|
'run_vm_command', 'inspect_vm', 'read_vm_file', 'write_vm_file', 'create_vm_file', 'cancel_vm_operation',
|
|
155
164
|
'create_function', 'invoke_function', 'confirm_service_order', 'confirm_domain_register',
|
|
156
165
|
]);
|
|
157
|
-
/** Keep
|
|
166
|
+
/** Keep management tools discoverable for incremental consent; order tools require granted authority. */
|
|
158
167
|
export function installRemotePolicy(server, scopes, profile = 'directory') {
|
|
159
168
|
const original = server.registerTool.bind(server);
|
|
160
169
|
const register = (name, config, handler) => {
|
|
161
170
|
const required = remoteScope(name, profile);
|
|
162
171
|
if (!required)
|
|
163
172
|
return undefined;
|
|
173
|
+
const orderTool = profile === 'orders' && orderPermissions.has(name);
|
|
174
|
+
if (orderTool && (!orderScopes.some(scope => scopes.includes(scope)) || !hasScope(scopes, required)))
|
|
175
|
+
return undefined;
|
|
164
176
|
const requiredScopes = required === "authenticated" ? [] : required === "vm:session"
|
|
165
177
|
? (scopes.some(s => s.startsWith("vm:")) ? scopes.filter(s => s.startsWith("vm:")) : ["vm:diagnostics"]) : [required];
|
|
166
178
|
const readOnly = name === 'validate_application_recipe' || required === "authenticated" || required.endsWith(":read") || required === "vm:diagnostics";
|
|
@@ -211,11 +223,13 @@ export function installRemotePolicy(server, scopes, profile = 'directory') {
|
|
|
211
223
|
_meta: { ...(config._meta || {}), securitySchemes },
|
|
212
224
|
}, async (...args) => {
|
|
213
225
|
const context = authorizationContext();
|
|
226
|
+
if (orderTool && (!context?.ordersAvailable || !hasScope(context.scopes, required))) {
|
|
227
|
+
return { isError: true, content: [{ type: 'text', text: 'This action is not available for this connection.' }] };
|
|
228
|
+
}
|
|
214
229
|
if (!context || !hasScope(context.scopes, required)) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
_meta: { "mcp/www_authenticate": [`Bearer error="insufficient_scope", error_description="Reconnect VPSnet to allow this action on your selected services.", scope="${requiredScopes.join(" ")}"`] } };
|
|
230
|
+
const message = `This action requires ${requiredScopes.join(' ')}. Ask the customer to approve this permission by re-authorizing this existing VPSnet connection in their AI app.`;
|
|
231
|
+
return { isError: true, content: [{ type: "text", text: message }],
|
|
232
|
+
_meta: { "mcp/www_authenticate": [`Bearer error="insufficient_scope", error_description="${message}", scope="${requiredScopes.join(" ")}"`] } };
|
|
219
233
|
}
|
|
220
234
|
let result;
|
|
221
235
|
try {
|