vpsnet-mcp 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bacarrdy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,291 @@
1
+ # vpsnet-mcp
2
+
3
+ [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for managing [VPSNet](https://www.vpsnet.com) virtual private servers. Gives AI assistants full control over your VPS infrastructure — start/stop servers, change plans, manage DNS, deploy SSH keys, order new servers, and more.
4
+
5
+ ## Features
6
+
7
+ - **49 tools** covering the entire VPSNet API
8
+ - Account & profile management
9
+ - VPS lifecycle (start, stop, restart, reinstall OS)
10
+ - Plan changes (free upgrades/downgrades)
11
+ - DNS (rDNS/PTR records)
12
+ - SSH key & API key management
13
+ - Backups, billing, invoices
14
+ - Ordering new VPS instances
15
+ - System status & pricing
16
+
17
+ ## Requirements
18
+
19
+ - Node.js 18 or newer
20
+ - A [VPSNet](https://www.vpsnet.com) account with an API key
21
+
22
+ ## Getting started
23
+
24
+ The standard config works across most MCP clients:
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "vpsnet": {
30
+ "command": "npx",
31
+ "args": ["-y", "vpsnet-mcp"],
32
+ "env": {
33
+ "VPSNET_API_KEY": "your_api_key_here"
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ <details>
41
+ <summary>Claude Code</summary>
42
+
43
+ ```bash
44
+ claude mcp add vpsnet -- npx -y vpsnet-mcp
45
+ ```
46
+
47
+ Set the environment variable before running:
48
+
49
+ ```bash
50
+ export VPSNET_API_KEY="your_api_key_here"
51
+ ```
52
+
53
+ </details>
54
+
55
+ <details>
56
+ <summary>Claude Desktop</summary>
57
+
58
+ Follow the [MCP install guide](https://modelcontextprotocol.io/quickstart/user), use the standard config above.
59
+
60
+ </details>
61
+
62
+ <details>
63
+ <summary>Cline</summary>
64
+
65
+ Open Cline MCP settings and add to your `cline_mcp_settings.json`:
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "vpsnet": {
71
+ "type": "stdio",
72
+ "command": "npx",
73
+ "args": ["-y", "vpsnet-mcp"],
74
+ "env": {
75
+ "VPSNET_API_KEY": "your_api_key_here"
76
+ },
77
+ "disabled": false
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ </details>
84
+
85
+ <details>
86
+ <summary>Codex</summary>
87
+
88
+ Use the Codex CLI:
89
+
90
+ ```bash
91
+ codex mcp add vpsnet npx "vpsnet-mcp"
92
+ ```
93
+
94
+ Or edit `~/.codex/config.toml`:
95
+
96
+ ```toml
97
+ [mcp_servers.vpsnet]
98
+ command = "npx"
99
+ args = ["vpsnet-mcp"]
100
+ ```
101
+
102
+ Set `VPSNET_API_KEY` in your shell environment before running Codex.
103
+
104
+ </details>
105
+
106
+ <details>
107
+ <summary>Cursor</summary>
108
+
109
+ Go to **Cursor Settings** > **MCP** > **Add new MCP Server**. Use command type with the command `npx -y vpsnet-mcp`. Or add manually to `.cursor/mcp.json`:
110
+
111
+ ```json
112
+ {
113
+ "mcpServers": {
114
+ "vpsnet": {
115
+ "command": "npx",
116
+ "args": ["-y", "vpsnet-mcp"],
117
+ "env": {
118
+ "VPSNET_API_KEY": "your_api_key_here"
119
+ }
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
125
+ </details>
126
+
127
+ <details>
128
+ <summary>Roo Code</summary>
129
+
130
+ Open Roo Code MCP settings and add to `roo_mcp_settings.json`:
131
+
132
+ ```json
133
+ {
134
+ "mcpServers": {
135
+ "vpsnet": {
136
+ "type": "stdio",
137
+ "command": "npx",
138
+ "args": ["-y", "vpsnet-mcp"],
139
+ "env": {
140
+ "VPSNET_API_KEY": "your_api_key_here"
141
+ },
142
+ "disabled": false
143
+ }
144
+ }
145
+ }
146
+ ```
147
+
148
+ </details>
149
+
150
+ <details>
151
+ <summary>VS Code (Copilot)</summary>
152
+
153
+ Install using the VS Code CLI:
154
+
155
+ ```bash
156
+ code --add-mcp '{"name":"vpsnet","command":"npx","args":["-y","vpsnet-mcp"],"env":{"VPSNET_API_KEY":"your_api_key_here"}}'
157
+ ```
158
+
159
+ Or add to your VS Code MCP config manually using the standard config above.
160
+
161
+ </details>
162
+
163
+ <details>
164
+ <summary>Windsurf</summary>
165
+
166
+ Follow the [Windsurf MCP documentation](https://docs.windsurf.com/windsurf/mcp). Use the standard config above.
167
+
168
+ </details>
169
+
170
+ ## Environment variables
171
+
172
+ | Variable | Required | Description |
173
+ |----------|----------|-------------|
174
+ | `VPSNET_API_KEY` | Yes | Your VPSNet API key |
175
+ | `VPSNET_API_URL` | No | API base URL (defaults to `https://api.vpsnet.com`) |
176
+
177
+ ## Tools
178
+
179
+ ### Account
180
+ | Tool | Description |
181
+ |------|-------------|
182
+ | `get_account` | Get account info (user ID, email, balance, VAT rate) |
183
+ | `get_profile` | Get user profile details (name, address, company) |
184
+
185
+ ### Services
186
+ | Tool | Description |
187
+ |------|-------------|
188
+ | `list_services` | List all active VPS services |
189
+ | `get_service` | Get detailed info for a service |
190
+ | `get_service_graphs` | Get performance graphs (CPU, RAM, disk, network) |
191
+ | `get_service_history` | Get action history for a service |
192
+
193
+ ### Service Actions
194
+ | Tool | Description |
195
+ |------|-------------|
196
+ | `start_service` | Start a stopped VPS |
197
+ | `stop_service` | Stop a running VPS |
198
+ | `restart_service` | Restart a VPS |
199
+
200
+ ### Service Settings
201
+ | Tool | Description |
202
+ |------|-------------|
203
+ | `change_hostname` | Change VPS hostname |
204
+ | `change_root_password` | Change VPS root password |
205
+ | `get_rdns` | Get current rDNS records |
206
+ | `change_rdns` | Change reverse DNS (PTR) record |
207
+ | `flush_iptables` | Flush iptables rules (useful when locked out) |
208
+ | `change_title` | Change service display title |
209
+ | `toggle_ipv6` | Enable or disable IPv6 |
210
+ | `toggle_extra_settings` | Toggle ppp, fuse, tuntap, or nfs |
211
+ | `deploy_ssh_key` | Deploy an SSH key to a VPS |
212
+
213
+ ### OS Reinstall
214
+ | Tool | Description |
215
+ |------|-------------|
216
+ | `get_os_options` | Get available OS templates |
217
+ | `reinstall_os` | Reinstall OS (destroys all data) |
218
+
219
+ ### Plan Changes (free)
220
+ | Tool | Description |
221
+ |------|-------------|
222
+ | `get_plan_options` | Get available plans for upgrade/downgrade |
223
+ | `get_plan_resources` | Get configurable resources for a plan |
224
+ | `calculate_plan_change` | Preview plan change cost and new expiry |
225
+ | `change_plan` | Change VPS plan |
226
+
227
+ ### Renewal & Billing
228
+ | Tool | Description |
229
+ |------|-------------|
230
+ | `get_period_options` | Get billing period and auto-renewal options |
231
+ | `set_auto_renew` | Enable or disable auto-renewal |
232
+ | `renew_service` | Manually renew a service |
233
+ | `list_invoices` | List invoices |
234
+ | `get_invoice` | Get a specific invoice |
235
+ | `list_payments` | List payment history |
236
+
237
+ ### Ordering
238
+ | Tool | Description |
239
+ |------|-------------|
240
+ | `get_order_plans` | Get available plans for new VPS |
241
+ | `get_order_options` | Get configurable options for a plan |
242
+ | `order_service` | Order a new VPS |
243
+
244
+ ### Backups
245
+ | Tool | Description |
246
+ |------|-------------|
247
+ | `get_backup_status` | Get backup status and configuration |
248
+ | `get_backup_history` | Get backup history |
249
+ | `create_backup` | Create a new backup (paid) |
250
+
251
+ ### SSH Keys
252
+ | Tool | Description |
253
+ |------|-------------|
254
+ | `list_ssh_keys` | List all SSH keys |
255
+ | `get_ssh_key` | Get a specific SSH key |
256
+ | `create_ssh_key` | Add a new SSH key |
257
+ | `delete_ssh_key` | Delete an SSH key |
258
+
259
+ ### API Keys
260
+ | Tool | Description |
261
+ |------|-------------|
262
+ | `list_api_keys` | List all API keys |
263
+ | `create_api_key` | Create a new API key |
264
+ | `update_api_key` | Update an existing API key |
265
+ | `revoke_api_key` | Revoke (delete) an API key |
266
+
267
+ ### History
268
+ | Tool | Description |
269
+ |------|-------------|
270
+ | `get_login_history` | Get account login history |
271
+ | `get_management_history` | Get management/activity history |
272
+
273
+ ### Public
274
+ | Tool | Description |
275
+ |------|-------------|
276
+ | `get_pricing` | Get public pricing |
277
+ | `get_system_status` | Get VPSNet system status |
278
+ | `get_faq` | Get frequently asked questions |
279
+
280
+ ## Getting an API key
281
+
282
+ 1. Log in at [vpsnet.com](https://www.vpsnet.com)
283
+ 2. Go to **Account** > **API Keys**
284
+ 3. Click **Create API Key**
285
+ 4. Set the scope to match your needs and copy the key
286
+
287
+ The key is shown only once — store it securely.
288
+
289
+ ## License
290
+
291
+ [MIT](LICENSE)
package/build/api.js ADDED
@@ -0,0 +1,24 @@
1
+ const API_BASE = process.env.VPSNET_API_URL || "https://api.vpsnet.com";
2
+ const API_KEY = process.env.VPSNET_API_KEY || "";
3
+ if (!API_KEY) {
4
+ console.error("VPSNET_API_KEY environment variable is required");
5
+ process.exit(1);
6
+ }
7
+ export async function apiRequest(method, path, body) {
8
+ const url = `${API_BASE}${path}`;
9
+ const headers = {
10
+ "X-API-KEY": API_KEY,
11
+ Accept: "application/json",
12
+ };
13
+ const init = { method, headers };
14
+ if (body) {
15
+ headers["Content-Type"] = "application/json";
16
+ init.body = JSON.stringify(body);
17
+ }
18
+ const res = await fetch(url, init);
19
+ const data = await res.json().catch(() => null);
20
+ return { status: res.status, data };
21
+ }
22
+ export function formatJson(data) {
23
+ return JSON.stringify(data, null, 2);
24
+ }
package/build/index.js ADDED
@@ -0,0 +1,669 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { apiRequest, formatJson } from "./api.js";
6
+ const server = new McpServer({ name: "vpsnet", version: "1.0.0" }, {
7
+ instructions: [
8
+ "This MCP server controls VPS (Virtual Private Server) services on VPSNet.",
9
+ "It manages VPS servers ONLY — not VDS or Dedicated Servers.",
10
+ "",
11
+ "## Ordering a new VPS",
12
+ "Flow: get_order_plans → get_order_options(plan) → order_service.",
13
+ "Payment object format: { payment: <numeric_id>, successUrl: '', cancelUrl: '' }.",
14
+ "For balance payment use payment ID 1: { payment: 1, successUrl: '', cancelUrl: '' }.",
15
+ "Resources is an array of numeric resource value IDs from get_order_options, e.g. [901, 907].",
16
+ "rootPassword rules: 6-40 chars, alphanumeric only, MUST contain uppercase + lowercase + digit. Example: 'MyPass123'.",
17
+ "sshKey and rootPassword are mutually exclusive — provide one or the other (or neither for auto-generated password).",
18
+ "After placing an order, wait 10-30 seconds before attempting SSH — the VPS needs time to boot and start SSH daemon.",
19
+ "",
20
+ "## SSH key workflow (IMPORTANT for deploying software to VPS)",
21
+ "When ordering or accessing a VPS, PREFER using sshKey over rootPassword — unless on Windows without an SSH client.",
22
+ "To access a VPS via SSH, you MUST use YOUR OWN local machine's SSH key:",
23
+ "1. Read the local machine's public key from ~/.ssh/id_rsa.pub or ~/.ssh/id_ed25519.pub",
24
+ "2. Upload it via create_ssh_key(name, public_key) — name it descriptively e.g. 'claude-code-local'",
25
+ "3. Deploy it to the VPS via deploy_ssh_key(orderNo, ssh_key_id) OR pass sshKey when ordering",
26
+ "4. Then SSH directly: ssh root@<vps_ip>",
27
+ "NEVER use someone else's SSH key — always read from the local machine where you are running.",
28
+ "Only fall back to rootPassword if the local machine has no SSH key and no SSH client (e.g. Windows without OpenSSH).",
29
+ "Note: order_service uses 'sshKey' (camelCase) but deploy_ssh_key uses 'ssh_key' (snake_case) — different field names!",
30
+ "To deploy software inside a VPS: first deploy an SSH key to the VPS, then access it directly via SSH. If no SSH client is available (e.g. Windows without OpenSSH), use rootPassword instead and connect via the platform's console or other remote access.",
31
+ "",
32
+ "## Plan changes",
33
+ "Plan changes are FREE — remaining time is recalculated (upgrade = shorter expiry, downgrade = longer expiry).",
34
+ "Flow: get_plan_options → get_plan_resources(orderNo, plan) → calculate_plan_change → change_plan.",
35
+ "Resources must be an array of numeric resource value IDs — one ID per resource type (RAM, SSD, CPU, Traffic, Bandwidth).",
36
+ "Get IDs from get_plan_resources: each resource type has a 'values' array, pick one value's 'id' per type.",
37
+ "Use isDefault=1 values for plan defaults. Do NOT pass an empty array — it will fail silently.",
38
+ "IP resources are typically disabled (managed by backend). Admin resources are auto-managed — do not include them.",
39
+ "",
40
+ "## Backups",
41
+ "Creating a backup is a PAID operation. Flow: get_backup_status → create_backup.",
42
+ "get_backup_status returns available period dates (up to 7 days in past) and price.",
43
+ "create_backup requires: period (YYYY-MM-DD date from options), directories (e.g. '/'), and payment object.",
44
+ "",
45
+ "## Async operations",
46
+ "All service actions (start/stop/restart/OS reinstall) are async — they return a noty UUID for tracking progress via WebSocket.",
47
+ "",
48
+ "## Renewal",
49
+ "Payment object for renewal is the same format: { payment: 1, successUrl: '', cancelUrl: '' } for balance.",
50
+ "Flow: get_period_options → renew_service(orderNo, period, payment).",
51
+ ].join("\n"),
52
+ });
53
+ // Helper to build service settings path
54
+ const svc = (orderNo, action) => `/account/services/${orderNo}/${action}`;
55
+ // --- Account ---
56
+ server.registerTool("get_account", {
57
+ description: "Get account info: user ID, email, balance, VAT rate",
58
+ inputSchema: {},
59
+ }, async () => {
60
+ const { data } = await apiRequest("GET", "/account/session");
61
+ return { content: [{ type: "text", text: formatJson(data) }] };
62
+ });
63
+ server.registerTool("get_profile", {
64
+ description: "Get user profile details (name, address, company info)",
65
+ inputSchema: {},
66
+ }, async () => {
67
+ const { data } = await apiRequest("GET", "/account/profile");
68
+ return { content: [{ type: "text", text: formatJson(data) }] };
69
+ });
70
+ // --- Services ---
71
+ server.registerTool("list_services", {
72
+ description: "List all active VPS services with state, plan, IPs, and expiry",
73
+ inputSchema: {},
74
+ }, async () => {
75
+ const { data } = await apiRequest("GET", "/account/services");
76
+ return { content: [{ type: "text", text: formatJson(data) }] };
77
+ });
78
+ server.registerTool("get_service", {
79
+ description: "Get detailed info for a service by order number",
80
+ inputSchema: {
81
+ orderNo: z.string().describe("Order number, e.g. VP57068"),
82
+ },
83
+ }, async ({ orderNo }) => {
84
+ const { data } = await apiRequest("GET", `/account/services/${orderNo}`);
85
+ return { content: [{ type: "text", text: formatJson(data) }] };
86
+ });
87
+ server.registerTool("get_service_graphs", {
88
+ description: "Get performance graphs (CPU, RAM, disk, network)",
89
+ inputSchema: {
90
+ orderNo: z.string().describe("Order number"),
91
+ },
92
+ }, async ({ orderNo }) => {
93
+ const { data } = await apiRequest("GET", `/account/services/${orderNo}/graphs`);
94
+ return { content: [{ type: "text", text: formatJson(data) }] };
95
+ });
96
+ server.registerTool("get_service_history", {
97
+ description: "Get action history for a service",
98
+ inputSchema: {
99
+ orderNo: z.string().describe("Order number"),
100
+ },
101
+ }, async ({ orderNo }) => {
102
+ const { data } = await apiRequest("GET", `/account/services/${orderNo}/history`);
103
+ return { content: [{ type: "text", text: formatJson(data) }] };
104
+ });
105
+ // --- Service Actions ---
106
+ server.registerTool("start_service", {
107
+ description: "Start a stopped VPS. Returns noty UUID for tracking.",
108
+ inputSchema: {
109
+ orderNo: z.string().describe("Order number"),
110
+ },
111
+ }, async ({ orderNo }) => {
112
+ const { data } = await apiRequest("POST", svc(orderNo, "start"));
113
+ return { content: [{ type: "text", text: formatJson(data) }] };
114
+ });
115
+ server.registerTool("stop_service", {
116
+ description: "Stop a running VPS. Returns noty UUID for tracking.",
117
+ inputSchema: {
118
+ orderNo: z.string().describe("Order number"),
119
+ },
120
+ }, async ({ orderNo }) => {
121
+ const { data } = await apiRequest("POST", svc(orderNo, "stop"));
122
+ return { content: [{ type: "text", text: formatJson(data) }] };
123
+ });
124
+ server.registerTool("restart_service", {
125
+ description: "Restart a VPS. Returns noty UUID for tracking.",
126
+ inputSchema: {
127
+ orderNo: z.string().describe("Order number"),
128
+ },
129
+ }, async ({ orderNo }) => {
130
+ const { data } = await apiRequest("POST", svc(orderNo, "restart"));
131
+ return { content: [{ type: "text", text: formatJson(data) }] };
132
+ });
133
+ // --- Service Settings ---
134
+ server.registerTool("change_hostname", {
135
+ description: "Change VPS hostname",
136
+ inputSchema: {
137
+ orderNo: z.string().describe("Order number"),
138
+ hostname: z.string().describe("New hostname"),
139
+ },
140
+ }, async ({ orderNo, hostname }) => {
141
+ const { data } = await apiRequest("POST", svc(orderNo, "change-hostname"), { hostname });
142
+ return { content: [{ type: "text", text: formatJson(data) }] };
143
+ });
144
+ server.registerTool("change_root_password", {
145
+ description: "Change VPS root password. Rules: 6-40 chars, alphanumeric, MUST contain uppercase + lowercase + digit. Example: 'MyPass123'.",
146
+ inputSchema: {
147
+ orderNo: z.string().describe("Order number"),
148
+ password: z
149
+ .string()
150
+ .describe("New root password. 6-40 chars, alphanumeric, must contain uppercase + lowercase + digit"),
151
+ },
152
+ }, async ({ orderNo, password }) => {
153
+ const { data } = await apiRequest("POST", svc(orderNo, "change-root-password"), { password });
154
+ return { content: [{ type: "text", text: formatJson(data) }] };
155
+ });
156
+ server.registerTool("get_rdns", {
157
+ description: "Get current rDNS records for a service",
158
+ inputSchema: {
159
+ orderNo: z.string().describe("Order number"),
160
+ },
161
+ }, async ({ orderNo }) => {
162
+ const { data } = await apiRequest("GET", svc(orderNo, "change-rdns"));
163
+ return { content: [{ type: "text", text: formatJson(data) }] };
164
+ });
165
+ server.registerTool("change_rdns", {
166
+ description: "Change reverse DNS record for a service IP. PTR value rules: min 3 chars, max 10 dot-separated labels, each label 1-30 chars (alphanumeric + hyphen, no leading/trailing hyphens). Blacklisted words in any label: 'vpsnet', 'speedy'. Use get_rdns first to see available IPs.",
167
+ inputSchema: {
168
+ orderNo: z.string().describe("Order number"),
169
+ ip: z
170
+ .string()
171
+ .describe("IP address to set rDNS for. Must belong to this service (check get_rdns)"),
172
+ value: z
173
+ .string()
174
+ .describe("New rDNS value (hostname). Valid FQDN, e.g. 'mail.example.com'. Labels: 1-30 chars, alphanumeric+hyphen, no leading/trailing hyphens. Cannot contain 'vpsnet' or 'speedy'"),
175
+ },
176
+ }, async ({ orderNo, ip, value }) => {
177
+ const { data } = await apiRequest("POST", svc(orderNo, "change-rdns"), { ip, value });
178
+ return { content: [{ type: "text", text: formatJson(data) }] };
179
+ });
180
+ server.registerTool("flush_iptables", {
181
+ description: "Flush iptables rules on VPS (useful when locked out)",
182
+ inputSchema: {
183
+ orderNo: z.string().describe("Order number"),
184
+ },
185
+ }, async ({ orderNo }) => {
186
+ const { data } = await apiRequest("POST", svc(orderNo, "flush-ip-tables"));
187
+ return { content: [{ type: "text", text: formatJson(data) }] };
188
+ });
189
+ server.registerTool("change_title", {
190
+ description: "Change service display title",
191
+ inputSchema: {
192
+ orderNo: z.string().describe("Order number"),
193
+ title: z.string().describe("New display title"),
194
+ },
195
+ }, async ({ orderNo, title }) => {
196
+ const { data } = await apiRequest("POST", svc(orderNo, "change-title"), { title });
197
+ return { content: [{ type: "text", text: formatJson(data) }] };
198
+ });
199
+ server.registerTool("toggle_ipv6", {
200
+ description: "Enable or disable IPv6 on VPS",
201
+ inputSchema: {
202
+ orderNo: z.string().describe("Order number"),
203
+ value: z.boolean().describe("true to enable, false to disable"),
204
+ },
205
+ }, async ({ orderNo, value }) => {
206
+ const { data } = await apiRequest("POST", svc(orderNo, "ipv6-toggle"), { value });
207
+ return { content: [{ type: "text", text: formatJson(data) }] };
208
+ });
209
+ server.registerTool("toggle_extra_settings", {
210
+ description: "Toggle extra VPS settings: ppp, fuse, tuntap, or nfs",
211
+ inputSchema: {
212
+ orderNo: z.string().describe("Order number"),
213
+ name: z
214
+ .enum(["ppp", "fuse", "tuntap", "nfs"])
215
+ .describe("Setting name"),
216
+ value: z.boolean().describe("true to enable, false to disable"),
217
+ },
218
+ }, async ({ orderNo, name, value }) => {
219
+ const { data } = await apiRequest("POST", svc(orderNo, "extra-settings-toggle"), { name, value });
220
+ return { content: [{ type: "text", text: formatJson(data) }] };
221
+ });
222
+ server.registerTool("deploy_ssh_key", {
223
+ description: "Deploy an SSH key to VPS. Use list_ssh_keys to get available key IDs. To add your own key first: read ~/.ssh/id_rsa.pub from local machine, then create_ssh_key, then deploy it here.",
224
+ inputSchema: {
225
+ orderNo: z.string().describe("Order number"),
226
+ ssh_key: z.number().describe("SSH key ID from list_ssh_keys"),
227
+ },
228
+ }, async ({ orderNo, ssh_key }) => {
229
+ const { data } = await apiRequest("POST", svc(orderNo, "change-ssh-key"), { ssh_key });
230
+ return { content: [{ type: "text", text: formatJson(data) }] };
231
+ });
232
+ // --- OS Reinstall ---
233
+ server.registerTool("get_os_options", {
234
+ description: "Get available OS templates for reinstall",
235
+ inputSchema: {
236
+ orderNo: z.string().describe("Order number"),
237
+ },
238
+ }, async ({ orderNo }) => {
239
+ const { data } = await apiRequest("GET", svc(orderNo, "change-os"));
240
+ return { content: [{ type: "text", text: formatJson(data) }] };
241
+ });
242
+ server.registerTool("reinstall_os", {
243
+ description: "Reinstall OS on VPS. WARNING: destroys all data! Returns noty UUID. Password rules: 6-40 chars, alphanumeric, must contain uppercase + lowercase + digit.",
244
+ inputSchema: {
245
+ orderNo: z.string().describe("Order number"),
246
+ osVersion: z
247
+ .number()
248
+ .describe("OS version ID from get_os_options"),
249
+ rootPassword: z
250
+ .string()
251
+ .optional()
252
+ .describe("New root password (auto-generated if omitted). 6-40 chars, alphanumeric, must contain uppercase + lowercase + digit"),
253
+ },
254
+ }, async ({ orderNo, osVersion, rootPassword }) => {
255
+ const body = { osVersion };
256
+ if (rootPassword)
257
+ body.rootPassword = rootPassword;
258
+ const { data } = await apiRequest("POST", svc(orderNo, "change-os"), body);
259
+ return { content: [{ type: "text", text: formatJson(data) }] };
260
+ });
261
+ // --- Plan Change (FREE) ---
262
+ server.registerTool("get_plan_options", {
263
+ description: "Get available plans for upgrade/downgrade. Plan changes are FREE.",
264
+ inputSchema: {
265
+ orderNo: z.string().describe("Order number"),
266
+ },
267
+ }, async ({ orderNo }) => {
268
+ const { data } = await apiRequest("GET", svc(orderNo, "plans-options"));
269
+ return { content: [{ type: "text", text: formatJson(data) }] };
270
+ });
271
+ server.registerTool("get_plan_resources", {
272
+ description: "Get configurable resources for a specific plan",
273
+ inputSchema: {
274
+ orderNo: z.string().describe("Order number"),
275
+ plan: z.number().describe("Plan ID from get_plan_options"),
276
+ },
277
+ }, async ({ orderNo, plan }) => {
278
+ const { data } = await apiRequest("GET", svc(orderNo, `plans-options/${plan}/options`));
279
+ return { content: [{ type: "text", text: formatJson(data) }] };
280
+ });
281
+ server.registerTool("calculate_plan_change", {
282
+ description: "Preview plan change cost and new expiry. Plan changes are FREE — recalculates remaining time. Use get_plan_resources first to see available resource IDs for the target plan.",
283
+ inputSchema: {
284
+ orderNo: z.string().describe("Order number"),
285
+ plan: z.number().describe("Plan ID from get_plan_options"),
286
+ resources: z
287
+ .array(z.number())
288
+ .describe("Array of numeric resource value IDs — one per resource type (RAM, SSD, IP, etc.). Get IDs from get_plan_resources response: each resource type has 'values' array, pick one value's 'id' per type. Use isDefault=1 values for defaults. Do NOT pass empty array."),
289
+ },
290
+ }, async ({ orderNo, plan, resources }) => {
291
+ const { data } = await apiRequest("POST", svc(orderNo, "plans-options/calculate"), { plan, resources });
292
+ return { content: [{ type: "text", text: formatJson(data) }] };
293
+ });
294
+ server.registerTool("change_plan", {
295
+ description: "Change VPS plan (FREE). Recalculates expiry based on price difference. Always call calculate_plan_change first to preview. Use get_plan_resources to get resource IDs for the target plan.",
296
+ inputSchema: {
297
+ orderNo: z.string().describe("Order number"),
298
+ plan: z.number().describe("Plan ID from get_plan_options"),
299
+ resources: z
300
+ .array(z.number())
301
+ .describe("Array of numeric resource value IDs — one per resource type (RAM, SSD, IP, etc.). Get IDs from get_plan_resources response: each resource type has 'values' array, pick one value's 'id' per type. Use isDefault=1 values for defaults. Do NOT pass empty array."),
302
+ },
303
+ }, async ({ orderNo, plan, resources }) => {
304
+ const { data } = await apiRequest("POST", svc(orderNo, "plans-options"), { plan, resources });
305
+ return { content: [{ type: "text", text: formatJson(data) }] };
306
+ });
307
+ // --- Period & Renewal ---
308
+ server.registerTool("get_period_options", {
309
+ description: "Get billing period and auto-renewal options",
310
+ inputSchema: {
311
+ orderNo: z.string().describe("Order number"),
312
+ },
313
+ }, async ({ orderNo }) => {
314
+ const { data } = await apiRequest("GET", svc(orderNo, "periods-options"));
315
+ return { content: [{ type: "text", text: formatJson(data) }] };
316
+ });
317
+ server.registerTool("set_auto_renew", {
318
+ description: "Enable or disable auto-renewal for a service",
319
+ inputSchema: {
320
+ orderNo: z.string().describe("Order number"),
321
+ state: z.boolean().describe("true to enable, false to disable"),
322
+ period: z
323
+ .number()
324
+ .optional()
325
+ .describe("Billing period ID (required when enabling)"),
326
+ },
327
+ }, async ({ orderNo, state, period }) => {
328
+ const body = { state };
329
+ if (period !== undefined)
330
+ body.period = period;
331
+ const { data } = await apiRequest("POST", svc(orderNo, "periods-options/auto-renew"), body);
332
+ return { content: [{ type: "text", text: formatJson(data) }] };
333
+ });
334
+ server.registerTool("renew_service", {
335
+ description: "Manually renew a service for a specific period. Payment object: { payment: 1, successUrl: '', cancelUrl: '' } for balance payment.",
336
+ inputSchema: {
337
+ orderNo: z.string().describe("Order number"),
338
+ period: z.number().describe("Period ID from get_period_options"),
339
+ payment: z
340
+ .object({
341
+ payment: z
342
+ .number()
343
+ .describe("Payment method ID. Use 1 for balance payment"),
344
+ successUrl: z
345
+ .string()
346
+ .describe("Redirect URL on success (use empty string '')"),
347
+ cancelUrl: z
348
+ .string()
349
+ .describe("Redirect URL on cancel (use empty string '')"),
350
+ })
351
+ .passthrough()
352
+ .describe("Payment object. For balance: { payment: 1, successUrl: '', cancelUrl: '' }"),
353
+ },
354
+ }, async ({ orderNo, period, payment }) => {
355
+ const { data } = await apiRequest("POST", svc(orderNo, "periods-options"), { period, payment });
356
+ return { content: [{ type: "text", text: formatJson(data) }] };
357
+ });
358
+ // --- Order New VPS ---
359
+ server.registerTool("get_order_plans", {
360
+ description: "Get available plans for ordering a new VPS",
361
+ inputSchema: {
362
+ type: z
363
+ .enum(["vps"])
364
+ .default("vps")
365
+ .describe("Service type (vps)"),
366
+ },
367
+ }, async ({ type }) => {
368
+ const { data } = await apiRequest("GET", `/order/configuration/${type}/plans`);
369
+ return { content: [{ type: "text", text: formatJson(data) }] };
370
+ });
371
+ server.registerTool("get_order_options", {
372
+ description: "Get configurable options (OS, resources, periods) for a plan",
373
+ inputSchema: {
374
+ plan: z.number().describe("Plan ID from get_order_plans"),
375
+ },
376
+ }, async ({ plan }) => {
377
+ const { data } = await apiRequest("GET", `/order/configuration/${plan}/options`);
378
+ return { content: [{ type: "text", text: formatJson(data) }] };
379
+ });
380
+ server.registerTool("order_service", {
381
+ description: [
382
+ "Order a new VPS. Requires sufficient account balance for balance payment.",
383
+ "Payment object for balance: { payment: 1, successUrl: '', cancelUrl: '' }.",
384
+ "Resources: array of numeric resource value IDs from get_order_options, e.g. [901, 907].",
385
+ "rootPassword: 6-40 chars, alphanumeric, must contain uppercase + lowercase + digit. Example: 'MyPass123'.",
386
+ "sshKey and rootPassword are mutually exclusive — provide one or the other.",
387
+ ].join(" "),
388
+ inputSchema: {
389
+ plan: z.number().describe("Plan ID from get_order_plans"),
390
+ os: z.number().optional().describe("OS version ID from get_order_options"),
391
+ rootPassword: z
392
+ .string()
393
+ .optional()
394
+ .describe("Root password. 6-40 chars, alphanumeric, must contain uppercase + lowercase + digit. Mutually exclusive with sshKey"),
395
+ sshKey: z
396
+ .number()
397
+ .optional()
398
+ .describe("SSH key ID from list_ssh_keys to deploy. Mutually exclusive with rootPassword"),
399
+ period: z.number().optional().describe("Billing period ID from get_order_options"),
400
+ resources: z
401
+ .array(z.number())
402
+ .optional()
403
+ .describe("Array of numeric resource value IDs from get_order_options, e.g. [901, 907, 902]"),
404
+ payment: z
405
+ .object({
406
+ payment: z
407
+ .number()
408
+ .describe("Payment method ID. Use 1 for balance payment"),
409
+ successUrl: z
410
+ .string()
411
+ .describe("Redirect URL on success (use empty string '')"),
412
+ cancelUrl: z
413
+ .string()
414
+ .describe("Redirect URL on cancel (use empty string '')"),
415
+ })
416
+ .passthrough()
417
+ .describe("Payment object. For balance: { payment: 1, successUrl: '', cancelUrl: '' }"),
418
+ },
419
+ }, async ({ plan, os, rootPassword, sshKey, period, resources, payment }) => {
420
+ const body = { plan, payment };
421
+ if (os !== undefined)
422
+ body.os = os;
423
+ if (rootPassword)
424
+ body.rootPassword = rootPassword;
425
+ if (sshKey !== undefined)
426
+ body.sshKey = sshKey;
427
+ if (period !== undefined)
428
+ body.period = period;
429
+ if (resources)
430
+ body.resources = resources;
431
+ const { data } = await apiRequest("POST", "/order/configuration/confirm", body);
432
+ return { content: [{ type: "text", text: formatJson(data) }] };
433
+ });
434
+ // --- Backups ---
435
+ server.registerTool("get_backup_status", {
436
+ description: "Get backup status and configuration for a service",
437
+ inputSchema: {
438
+ orderNo: z.string().describe("Order number"),
439
+ },
440
+ }, async ({ orderNo }) => {
441
+ const { data } = await apiRequest("GET", svc(orderNo, "backup/status"));
442
+ return { content: [{ type: "text", text: formatJson(data) }] };
443
+ });
444
+ server.registerTool("get_backup_history", {
445
+ description: "Get backup history for a service",
446
+ inputSchema: {
447
+ orderNo: z.string().describe("Order number"),
448
+ },
449
+ }, async ({ orderNo }) => {
450
+ const { data } = await apiRequest("GET", svc(orderNo, "backup/history"));
451
+ return { content: [{ type: "text", text: formatJson(data) }] };
452
+ });
453
+ server.registerTool("create_backup", {
454
+ description: "Create a new backup. Returns noty UUID for tracking. First call get_backup_status to see available period dates and price. Backup is a paid operation (price shown in get_backup_status).",
455
+ inputSchema: {
456
+ orderNo: z.string().describe("Order number"),
457
+ period: z
458
+ .string()
459
+ .describe("Backup date in YYYY-MM-DD format. Must be one of the dates from get_backup_status options (up to 7 days in the past)"),
460
+ directories: z
461
+ .string()
462
+ .describe("Directories to backup, e.g. '/' for full backup"),
463
+ payment: z
464
+ .object({
465
+ payment: z
466
+ .number()
467
+ .describe("Payment method ID. Use 1 for balance payment"),
468
+ successUrl: z
469
+ .string()
470
+ .describe("Redirect URL on success (use empty string '')"),
471
+ cancelUrl: z
472
+ .string()
473
+ .describe("Redirect URL on cancel (use empty string '')"),
474
+ })
475
+ .passthrough()
476
+ .describe("Payment object. For balance: { payment: 1, successUrl: '', cancelUrl: '' }"),
477
+ },
478
+ }, async ({ orderNo, period, directories, payment }) => {
479
+ const { data } = await apiRequest("POST", svc(orderNo, "backup"), {
480
+ period,
481
+ directories,
482
+ payment,
483
+ });
484
+ return { content: [{ type: "text", text: formatJson(data) }] };
485
+ });
486
+ // --- SSH Keys ---
487
+ server.registerTool("list_ssh_keys", {
488
+ description: "List all SSH keys on the account",
489
+ inputSchema: {},
490
+ }, async () => {
491
+ const { data } = await apiRequest("GET", "/account/ssh-keys");
492
+ return { content: [{ type: "text", text: formatJson(data) }] };
493
+ });
494
+ server.registerTool("get_ssh_key", {
495
+ description: "Get a specific SSH key by ID",
496
+ inputSchema: {
497
+ id: z.number().describe("SSH key ID"),
498
+ },
499
+ }, async ({ id }) => {
500
+ const { data } = await apiRequest("GET", `/account/ssh-keys/${id}`);
501
+ return { content: [{ type: "text", text: formatJson(data) }] };
502
+ });
503
+ server.registerTool("create_ssh_key", {
504
+ description: "Add a new SSH key to the account. To deploy software on a VPS, read the local machine's public key from ~/.ssh/id_rsa.pub or ~/.ssh/id_ed25519.pub, upload it here, then deploy_ssh_key to the VPS.",
505
+ inputSchema: {
506
+ name: z.string().describe("Key name/label"),
507
+ public_key: z.string().describe("SSH public key content"),
508
+ },
509
+ }, async ({ name, public_key }) => {
510
+ const { data } = await apiRequest("POST", "/account/ssh-keys", {
511
+ name,
512
+ public_key,
513
+ });
514
+ return { content: [{ type: "text", text: formatJson(data) }] };
515
+ });
516
+ server.registerTool("delete_ssh_key", {
517
+ description: "Delete an SSH key from the account",
518
+ inputSchema: {
519
+ id: z.number().describe("SSH key ID"),
520
+ },
521
+ }, async ({ id }) => {
522
+ const { data } = await apiRequest("DELETE", `/account/ssh-keys/${id}`);
523
+ return { content: [{ type: "text", text: formatJson(data) }] };
524
+ });
525
+ // --- API Keys ---
526
+ server.registerTool("list_api_keys", {
527
+ description: "List all API keys on the account",
528
+ inputSchema: {},
529
+ }, async () => {
530
+ const { data } = await apiRequest("GET", "/account/api-keys");
531
+ return { content: [{ type: "text", text: formatJson(data) }] };
532
+ });
533
+ server.registerTool("create_api_key", {
534
+ description: "Create a new API key",
535
+ inputSchema: {
536
+ name: z.string().describe("Key name"),
537
+ scope: z.string().describe("Permission scope"),
538
+ allowed_ips: z
539
+ .string()
540
+ .optional()
541
+ .describe("Comma-separated allowed IPs"),
542
+ expires_at: z
543
+ .string()
544
+ .optional()
545
+ .describe("Expiry date (YYYY-MM-DD)"),
546
+ },
547
+ }, async ({ name, scope, allowed_ips, expires_at }) => {
548
+ const body = { name, scope };
549
+ if (allowed_ips)
550
+ body.allowed_ips = allowed_ips;
551
+ if (expires_at)
552
+ body.expires_at = expires_at;
553
+ const { data } = await apiRequest("POST", "/account/api-keys", body);
554
+ return { content: [{ type: "text", text: formatJson(data) }] };
555
+ });
556
+ server.registerTool("update_api_key", {
557
+ description: "Update an existing API key",
558
+ inputSchema: {
559
+ id: z.number().describe("API key ID"),
560
+ name: z.string().describe("Key name"),
561
+ scope: z.string().describe("Permission scope"),
562
+ allowed_ips: z
563
+ .string()
564
+ .optional()
565
+ .describe("Comma-separated allowed IPs"),
566
+ expires_at: z
567
+ .string()
568
+ .optional()
569
+ .describe("Expiry date (YYYY-MM-DD)"),
570
+ },
571
+ }, async ({ id, name, scope, allowed_ips, expires_at }) => {
572
+ const body = { name, scope };
573
+ if (allowed_ips)
574
+ body.allowed_ips = allowed_ips;
575
+ if (expires_at)
576
+ body.expires_at = expires_at;
577
+ const { data } = await apiRequest("POST", `/account/api-keys/${id}`, body);
578
+ return { content: [{ type: "text", text: formatJson(data) }] };
579
+ });
580
+ server.registerTool("revoke_api_key", {
581
+ description: "Revoke (delete) an API key",
582
+ inputSchema: {
583
+ id: z.number().describe("API key ID"),
584
+ },
585
+ }, async ({ id }) => {
586
+ const { data } = await apiRequest("DELETE", `/account/api-keys/${id}`);
587
+ return { content: [{ type: "text", text: formatJson(data) }] };
588
+ });
589
+ // --- Billing ---
590
+ server.registerTool("list_invoices", {
591
+ description: "List invoices with pagination",
592
+ inputSchema: {
593
+ page: z.number().optional().describe("Page number"),
594
+ },
595
+ }, async ({ page }) => {
596
+ const query = page ? `?page=${page}` : "";
597
+ const { data } = await apiRequest("GET", `/account/history/invoices${query}`);
598
+ return { content: [{ type: "text", text: formatJson(data) }] };
599
+ });
600
+ server.registerTool("get_invoice", {
601
+ description: "Get a specific invoice by hash",
602
+ inputSchema: {
603
+ hash: z.string().describe("Invoice hash"),
604
+ },
605
+ }, async ({ hash }) => {
606
+ const { data } = await apiRequest("GET", `/account/history/invoices/${hash}`);
607
+ return { content: [{ type: "text", text: formatJson(data) }] };
608
+ });
609
+ server.registerTool("list_payments", {
610
+ description: "List payment history with pagination",
611
+ inputSchema: {
612
+ page: z.number().optional().describe("Page number"),
613
+ },
614
+ }, async ({ page }) => {
615
+ const query = page ? `?page=${page}` : "";
616
+ const { data } = await apiRequest("GET", `/account/history/payments${query}`);
617
+ return { content: [{ type: "text", text: formatJson(data) }] };
618
+ });
619
+ // --- History ---
620
+ server.registerTool("get_login_history", {
621
+ description: "Get account login history (IPs, dates)",
622
+ inputSchema: {},
623
+ }, async () => {
624
+ const { data } = await apiRequest("GET", "/account/history/login");
625
+ return { content: [{ type: "text", text: formatJson(data) }] };
626
+ });
627
+ server.registerTool("get_management_history", {
628
+ description: "Get management/activity history (service actions, changes)",
629
+ inputSchema: {},
630
+ }, async () => {
631
+ const { data } = await apiRequest("GET", "/account/history/management");
632
+ return { content: [{ type: "text", text: formatJson(data) }] };
633
+ });
634
+ // --- Public ---
635
+ server.registerTool("get_pricing", {
636
+ description: "Get public pricing for a service type",
637
+ inputSchema: {
638
+ type: z
639
+ .enum(["vps", "vds", "ds", "vps_storage"])
640
+ .describe("Service type"),
641
+ },
642
+ }, async ({ type }) => {
643
+ const { data } = await apiRequest("GET", `/public/prices/${type}/plans`);
644
+ return { content: [{ type: "text", text: formatJson(data) }] };
645
+ });
646
+ server.registerTool("get_system_status", {
647
+ description: "Get VPSNet system status",
648
+ inputSchema: {},
649
+ }, async () => {
650
+ const { data } = await apiRequest("GET", "/public/status");
651
+ return { content: [{ type: "text", text: formatJson(data) }] };
652
+ });
653
+ server.registerTool("get_faq", {
654
+ description: "Get frequently asked questions",
655
+ inputSchema: {},
656
+ }, async () => {
657
+ const { data } = await apiRequest("GET", "/public/faq");
658
+ return { content: [{ type: "text", text: formatJson(data) }] };
659
+ });
660
+ // --- Start server ---
661
+ async function main() {
662
+ const transport = new StdioServerTransport();
663
+ await server.connect(transport);
664
+ console.error("VPSNet MCP server running");
665
+ }
666
+ main().catch((error) => {
667
+ console.error("Fatal error:", error);
668
+ process.exit(1);
669
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "vpsnet-mcp",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "MCP server for managing VPSNet virtual private servers",
6
+ "bin": {
7
+ "vpsnet-mcp": "./build/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsc --watch",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "files": ["build"],
15
+ "repository": "github:bacarrdy/vpsnet-mcp",
16
+ "homepage": "https://github.com/bacarrdy/vpsnet-mcp",
17
+ "author": "bacarrdy",
18
+ "license": "MIT",
19
+ "keywords": [
20
+ "mcp",
21
+ "vps",
22
+ "vpsnet",
23
+ "model-context-protocol",
24
+ "claude",
25
+ "server-management",
26
+ "hosting"
27
+ ],
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "^1.12.1",
30
+ "zod": "^3.24.2"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.13.1",
34
+ "typescript": "^5.7.3"
35
+ }
36
+ }