vpsnet-mcp 2.3.3 → 2.3.4

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.
@@ -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, scopes: value.scope.split(" ").filter(x => (allowedScopes.includes(x) || x === "vm:files")), services: value.services };
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.",
@@ -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. Reconnect VPSnet in your AI app.' : 'This action needs an additional permission. Reconnect VPSnet and approve it.');
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
  }
@@ -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. If a needed permission is missing, reconnect through OAuth and let the customer select it; never work around a denial.",
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 const orderInstructions = remoteInstructions.replace("This connector does not purchase capacity, change payment settings or run model inference.", "This VPSnet connection can purchase permanent VPS servers and register domains from the customer’s VPSnet balance when purchase permissions are enabled. Use the existing connection; its owner can change purchase permissions in the VPSnet panel without reconnecting. Quote first, show the final gross EUR amount and period, and confirm only after explicit approval. 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.");
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 supported tools discoverable for incremental OAuth consent; purchases stay outside this profile. */
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
- if (required.startsWith('orders:'))
216
- return { isError: true, content: [{ type: 'text', text: 'Ordering permission is disabled. The customer can enable purchases and spending limits for this existing connection in VPSnet → AI connections → Purchase permissions. Do not add another connection. After permission is enabled, show the exact quote and ask for approval of the balance charge.' }] };
217
- return { isError: true, content: [{ type: "text", text: "Reconnect VPSnet with the required permissions." }],
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vpsnet-mcp",
3
- "version": "2.3.3",
3
+ "version": "2.3.4",
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": {