feat: initial implementation of VirtFusion MCP server
All checks were successful
Endpoint Sync Check / check-drift (push) Successful in 20s
CI / build (push) Successful in 26s

Complete MCP server wrapping all 84 VirtFusion Admin API endpoints:
- Core HTTP client with Bearer auth and error handling
- 17 tool modules organized by API category
- Endpoint drift detection scripts and Gitea Actions CI/CD
- Comprehensive README with configuration examples
- CLAUDE.md for AI assistant onboarding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 23:34:48 -04:00
commit 40d5e8161a
35 changed files with 13070 additions and 0 deletions

25
.gitea/workflows/ci.yaml Normal file
View File

@@ -0,0 +1,25 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build

View File

@@ -0,0 +1,27 @@
name: Endpoint Sync Check
on:
schedule:
- cron: '0 9 * * 1' # Every Monday at 9am
push:
paths:
- 'openapi.yaml'
workflow_dispatch:
jobs:
check-drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Check for endpoint drift
run: npm run check-endpoint-drift

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules/
dist/
*.js.map
*.d.ts.map
.env
.env.*
!.env.example

81
CLAUDE.md Normal file
View File

@@ -0,0 +1,81 @@
# VirtFusion MCP Server
## Overview
MCP server wrapping the VirtFusion virtualization control panel REST API (84 endpoints). Built with TypeScript, `@modelcontextprotocol/sdk`, and Zod for input validation.
## Architecture
```
src/
index.ts — Entry point: env validation, McpServer + StdioServerTransport setup
client.ts — VirtFusionClient: fetch wrapper with Bearer auth, query param filtering
errors.ts — VirtFusionApiError class + formatErrorResponse() for MCP error results
types.ts — Shared interfaces (PaginatedResponse, QueryParams)
tools/
index.ts — Barrel: registerAllTools() wires all 17 modules to McpServer
general.ts — 1 tool (test connection)
hypervisors.ts — 2 tools
hypervisor-groups.ts — 3 tools
servers.ts — 18 tools (largest module)
servers-power.ts — 4 tools
servers-network.ts — 5 tools
servers-firewall.ts — 4 tools
servers-traffic.ts — 4 tools
ip-blocks.ts — 3 tools
backups.ts — 1 tool
dns.ts — 1 tool
media.ts — 2 tools
packages.ts — 2 tools
queue.ts — 1 tool
ssh-keys.ts — 4 tools
users.ts — 7 tools
self-service.ts — 19 tools
```
## Conventions
### Tool naming
- `snake_case`: `{category}_{action}_{noun}` (e.g., `servers_power_boot`, `ip_blocks_list`)
- Every tool gets a human-readable description as second arg to `server.tool()`
### Module pattern
Each `src/tools/*.ts` file exports one function:
```typescript
export function registerXxxTools(server: McpServer, client: VirtFusionClient): void {
server.tool('tool_name', 'Description', { ...zodSchema }, async (params) => { ... });
}
```
### Error handling
- All tool handlers wrap logic in try/catch
- Catch block returns `formatErrorResponse(error)` which produces `{ isError: true, content: [...] }`
- `VirtFusionApiError` carries statusCode, statusText, and errorBody
### HTTP client
- `VirtFusionClient` methods: `get()`, `post()`, `put()`, `delete()`
- All accept `(path, body?, query?)` — query params with `undefined` values are filtered out
- 204 responses return `{ success: true }`
## How to add a new tool
1. Identify the API endpoint in `openapi.yaml`
2. Find (or create) the matching `src/tools/*.ts` module by tag
3. Add a `server.tool()` call following the existing pattern
4. Register it in `src/tools/index.ts` if it's a new module
5. Update `endpoint-manifest.json`: `npm run extract-endpoints > endpoint-manifest.json`
6. Update the tool count in README.md
## Commands
```bash
npm run build # Compile TypeScript to dist/
npm run dev # Run with tsx (development)
npm run extract-endpoints # Parse openapi.yaml → stdout JSON
npm run check-endpoint-drift # Compare spec vs manifest
```
## Environment variables
- `VIRTFUSION_API_URL` — Required. e.g., `https://cp.example.com/api/v1`
- `VIRTFUSION_API_TOKEN` — Required. Bearer token from VirtFusion admin panel.

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 EZSCALE Hosting, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

301
README.md Normal file
View File

@@ -0,0 +1,301 @@
# VirtFusion MCP Server
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that wraps the [VirtFusion](https://virtfusion.com) virtualization control panel API. This enables AI assistants like Claude to manage VirtFusion infrastructure through natural language.
## Features
- **84 tools** covering the entire VirtFusion Admin API
- Server lifecycle management (create, build, delete, suspend, unsuspend)
- Power control (boot, shutdown, restart, poweroff)
- Network management (IPv4, whitelist, firewall, traffic)
- User management with external relation ID support
- Self-service operations (credit, resource packs, hourly profiles)
- Infrastructure management (hypervisors, IP blocks, packages, DNS)
- Backup and media management
- SSH key management
- Automatic API endpoint drift detection
## Prerequisites
- **Node.js** 22 or later
- **VirtFusion** panel with Admin API access
- An API token generated from your VirtFusion admin panel
## Installation
### From source
```bash
git clone https://git.ezscale.cloud/EZSCALE/virtfusion-mcp.git
cd virtfusion-mcp
npm install
npm run build
```
## Configuration
The server requires two environment variables:
| Variable | Description | Example |
|---|---|---|
| `VIRTFUSION_API_URL` | Base URL of your VirtFusion API | `https://cp.example.com/api/v1` |
| `VIRTFUSION_API_TOKEN` | Admin API bearer token | `your-api-token-here` |
### Claude Desktop
Add to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"virtfusion": {
"command": "node",
"args": ["/path/to/virtfusion-mcp/dist/index.js"],
"env": {
"VIRTFUSION_API_URL": "https://cp.example.com/api/v1",
"VIRTFUSION_API_TOKEN": "your-api-token"
}
}
}
}
```
### Claude Code
```bash
claude mcp add virtfusion -- node /path/to/virtfusion-mcp/dist/index.js \
-e VIRTFUSION_API_URL=https://cp.example.com/api/v1 \
-e VIRTFUSION_API_TOKEN=your-api-token
```
### VS Code / Cursor
Add to your `.vscode/mcp.json`:
```json
{
"servers": {
"virtfusion": {
"command": "node",
"args": ["/path/to/virtfusion-mcp/dist/index.js"],
"env": {
"VIRTFUSION_API_URL": "https://cp.example.com/api/v1",
"VIRTFUSION_API_TOKEN": "your-api-token"
}
}
}
}
```
## Tool Reference
### General (1 tool)
| Tool | Description |
|---|---|
| `general_test_connection` | Test the API connection to VirtFusion |
### Servers (18 tools)
| Tool | Description |
|---|---|
| `servers_list` | List all servers with optional filtering |
| `servers_create` | Create a new server |
| `servers_get` | Retrieve details of a specific server |
| `servers_delete` | Delete a server |
| `servers_build` | Build/rebuild a server with an OS template |
| `servers_list_templates` | List OS templates available for a server |
| `servers_modify_name` | Change a server's display name |
| `servers_modify_cpu` | Modify CPU cores for a server |
| `servers_modify_memory` | Modify memory allocation for a server |
| `servers_throttle_cpu` | Throttle a server's CPU |
| `servers_get_traffic` | Retrieve traffic statistics |
| `servers_suspend` | Suspend a server |
| `servers_unsuspend` | Unsuspend a server |
| `servers_reset_password` | Reset a server's root/admin password |
| `servers_enable_vnc` | Enable or disable VNC |
| `servers_get_vnc` | Retrieve VNC connection details |
| `servers_set_custom_xml` | Set custom XML configuration |
| `servers_change_owner` | Change the owner of a server |
| `servers_change_package` | Change a server's package |
| `servers_set_backup_plan` | Set a server's backup plan |
| `servers_list_by_user` | List servers owned by a user |
### Servers — Power (4 tools)
| Tool | Description |
|---|---|
| `servers_power_boot` | Boot a server |
| `servers_power_shutdown` | Gracefully shutdown a server |
| `servers_power_restart` | Restart a server |
| `servers_power_poweroff` | Force power off a server |
### Servers — Network (5 tools)
| Tool | Description |
|---|---|
| `servers_network_whitelist_add` | Add an IP to a server's network whitelist |
| `servers_network_whitelist_remove` | Remove an IP from a server's whitelist |
| `servers_network_ipv4_add` | Add specific IPv4 addresses |
| `servers_network_ipv4_remove` | Remove IPv4 addresses |
| `servers_network_ipv4_add_qty` | Add a quantity of IPv4 addresses |
### Servers — Firewall (4 tools)
| Tool | Description |
|---|---|
| `servers_firewall_get` | Retrieve firewall configuration |
| `servers_firewall_enable` | Enable the firewall |
| `servers_firewall_disable` | Disable the firewall |
| `servers_firewall_set_rules` | Apply firewall rulesets |
### Servers — Traffic (4 tools)
| Tool | Description |
|---|---|
| `servers_traffic_list_blocks` | List traffic blocks |
| `servers_traffic_add_block` | Add a traffic block |
| `servers_traffic_remove_block` | Remove a traffic block |
| `servers_traffic_modify` | Modify primary traffic allowance |
### Hypervisors (2 tools)
| Tool | Description |
|---|---|
| `hypervisors_list` | List all hypervisors |
| `hypervisors_get` | Retrieve hypervisor details |
### Hypervisor Groups (3 tools)
| Tool | Description |
|---|---|
| `hypervisor_groups_list` | List all hypervisor groups |
| `hypervisor_groups_get` | Retrieve group details |
| `hypervisor_groups_get_resources` | Retrieve group resources |
### IP Blocks (3 tools)
| Tool | Description |
|---|---|
| `ip_blocks_list` | List all IP blocks |
| `ip_blocks_get` | Retrieve IP block details |
| `ip_blocks_add_ipv4_range` | Add an IPv4 range to a block |
### Packages (2 tools)
| Tool | Description |
|---|---|
| `packages_list` | List all packages |
| `packages_get` | Retrieve package details |
### Queue (1 tool)
| Tool | Description |
|---|---|
| `queue_get` | Retrieve queue item status |
### Backups (1 tool)
| Tool | Description |
|---|---|
| `backups_list_by_server` | List backups for a server |
### DNS (1 tool)
| Tool | Description |
|---|---|
| `dns_get_service` | Retrieve a DNS service |
### Media (2 tools)
| Tool | Description |
|---|---|
| `media_get_iso` | Retrieve ISO image details |
| `media_get_templates_by_package` | List OS templates for a package |
### SSH Keys (4 tools)
| Tool | Description |
|---|---|
| `ssh_keys_create` | Add an SSH key to a user |
| `ssh_keys_get` | Retrieve SSH key details |
| `ssh_keys_delete` | Delete an SSH key |
| `ssh_keys_list_by_user` | List SSH keys for a user |
### Users (7 tools)
| Tool | Description |
|---|---|
| `users_create` | Create a new user |
| `users_get` | Retrieve a user by external relation ID |
| `users_modify` | Modify a user |
| `users_delete` | Delete a user |
| `users_reset_password` | Reset a user's password |
| `users_generate_auth_tokens` | Generate login tokens |
| `users_generate_server_auth_tokens` | Generate server-scoped login tokens |
### Self Service (19 tools)
| Tool | Description |
|---|---|
| `self_service_add_credit` | Add credit to a user |
| `self_service_cancel_credit` | Cancel applied credit |
| `self_service_get_currencies` | Retrieve available currencies |
| `self_service_add_hourly_group_profile` | Add hourly group profile to user |
| `self_service_remove_hourly_group_profile` | Remove hourly group profile |
| `self_service_add_resource_group_profile` | Add resource group profile |
| `self_service_remove_resource_group_profile` | Remove resource group profile |
| `self_service_add_resource_pack` | Add resource pack to user |
| `self_service_get_resource_pack` | Retrieve a resource pack |
| `self_service_modify_resource_pack` | Modify a resource pack |
| `self_service_delete_resource_pack` | Delete a resource pack |
| `self_service_get_hourly_stats` | Retrieve hourly statistics |
| `self_service_set_hourly_resource_pack` | Set hourly resource pack |
| `self_service_modify_access` | Modify self-service access |
| `self_service_get_report` | Generate a user report |
| `self_service_get_usage` | Retrieve user usage data |
| `self_service_delete_pack_servers` | Delete all servers in a pack |
| `self_service_suspend_pack_servers` | Suspend all servers in a pack |
| `self_service_unsuspend_pack_servers` | Unsuspend all servers in a pack |
## Development
### Build
```bash
npm run build
```
### Run in development mode
```bash
VIRTFUSION_API_URL=https://cp.example.com/api/v1 \
VIRTFUSION_API_TOKEN=your-token \
npm run dev
```
### API Endpoint Sync
This project includes tooling to detect when VirtFusion adds new API endpoints:
```bash
# Extract endpoints from the current OpenAPI spec
npm run extract-endpoints > endpoint-manifest.json
# Check for drift between spec and manifest
npm run check-endpoint-drift
```
The `endpoint-sync` workflow runs weekly and on any changes to `openapi.yaml`, creating an issue if drift is detected.
#### Adding new tools when VirtFusion updates their API
1. Download the updated `openapi.yaml` from VirtFusion docs
2. Run `npm run check-endpoint-drift` to see what changed
3. Implement new tools in the appropriate `src/tools/*.ts` module
4. Update the manifest: `npm run extract-endpoints > endpoint-manifest.json`
5. Commit everything together
## License
MIT — see [LICENSE](LICENSE)

506
endpoint-manifest.json Normal file
View File

@@ -0,0 +1,506 @@
[
{
"method": "GET",
"path": "/backups/server/{serverId}",
"summary": "Retrieve a server backups",
"tag": "Backups"
},
{
"method": "GET",
"path": "/compute/hypervisors",
"summary": "Retrieve hypervisors",
"tag": "Hypervisors"
},
{
"method": "GET",
"path": "/compute/hypervisors/{hypervisorId}",
"summary": "Retrive a Hypervisor",
"tag": "Hypervisors"
},
{
"method": "GET",
"path": "/compute/hypervisors/groups",
"summary": "Retrieve hypervisor groups",
"tag": "Hypervisor Groups"
},
{
"method": "GET",
"path": "/compute/hypervisors/groups/{hypervisorGroupId}",
"summary": "Retrieve a hypervisor group",
"tag": "Hypervisor Groups"
},
{
"method": "GET",
"path": "/compute/hypervisors/groups/{hypervisorGroupId}/resources",
"summary": "Retrieve a hypervisor groups resources",
"tag": "Hypervisor Groups"
},
{
"method": "GET",
"path": "/connect",
"summary": "Test connection",
"tag": "General"
},
{
"method": "GET",
"path": "/connectivity/ipblocks",
"summary": "Retrieve IP blocks",
"tag": "IP Blocks"
},
{
"method": "GET",
"path": "/connectivity/ipblocks/{blockId}",
"summary": "Retrieve an IP block",
"tag": "IP Blocks"
},
{
"method": "POST",
"path": "/connectivity/ipblocks/{blockId}/ipv4",
"summary": "Add an IPv4 range to an IP block",
"tag": "IP Blocks"
},
{
"method": "GET",
"path": "/dns/services/{serviceId}",
"summary": "Retrieve a DNS service",
"tag": "DNS"
},
{
"method": "GET",
"path": "/media/iso/{isoId}",
"summary": "Retrieve an ISO",
"tag": "Media"
},
{
"method": "GET",
"path": "/media/templates/fromServerPackageSpec/{serverPackageId}",
"summary": "Retrieve operating system templates that are available for a package",
"tag": "Media"
},
{
"method": "GET",
"path": "/packages",
"summary": "Retrieve packages",
"tag": "Packages"
},
{
"method": "GET",
"path": "/packages/{packageId}",
"summary": "Retrieve a packge",
"tag": "Packages"
},
{
"method": "GET",
"path": "/queue/{queueId}",
"summary": "Retrieve a queue item",
"tag": "Queue & Tasks"
},
{
"method": "PUT",
"path": "/selfService/access/byUserExtRelationId/{extRelationId}",
"summary": "Modify user access",
"tag": "Self Service/External Relational ID"
},
{
"method": "DELETE",
"path": "/selfService/credit/{creditId}",
"summary": "Cancel credit that was applied to a user",
"tag": "Self Service"
},
{
"method": "POST",
"path": "/selfService/credit/byUserExtRelationId/{extRelationId}",
"summary": "Add credit to user",
"tag": "Self Service/External Relational ID"
},
{
"method": "GET",
"path": "/selfService/currencies",
"summary": "Retrieve currencies",
"tag": "Self Service"
},
{
"method": "DELETE",
"path": "/selfService/hourlyGroupProfile/{profileId}/byUserExtRelationId/{extRelationId}",
"summary": "Remove hourly group profile from a user",
"tag": "Self Service/External Relational ID"
},
{
"method": "POST",
"path": "/selfService/hourlyGroupProfile/byUserExtRelationId/{extRelationId}",
"summary": "Add an hourly group profile to a user",
"tag": "Self Service/External Relational ID"
},
{
"method": "PUT",
"path": "/selfService/hourlyResourcePack/byUserExtRelationId/{extRelationId}",
"summary": "Set an hourly resource pack",
"tag": "Self Service/External Relational ID"
},
{
"method": "GET",
"path": "/selfService/hourlyStats/byUserExtRelationId/{extRelationId}",
"summary": "Retrieve hourly statistics",
"tag": "Self Service/External Relational ID"
},
{
"method": "GET",
"path": "/selfService/report/byUserExtRelationId/{extRelationId}",
"summary": "Generate a report",
"tag": "Self Service/External Relational ID"
},
{
"method": "DELETE",
"path": "/selfService/resourceGroupProfile/{profileId}/byUserExtRelationId/{extRelationId}",
"summary": "Remove resource group from a user",
"tag": "Self Service/External Relational ID"
},
{
"method": "POST",
"path": "/selfService/resourceGroupProfile/byUserExtRelationId/{extRelationId}",
"summary": "Add a resource group profile to a user",
"tag": "Self Service/External Relational ID"
},
{
"method": "DELETE",
"path": "/selfService/resourcePack/{packId}",
"summary": "Delete a user resource pack",
"tag": "Self Service"
},
{
"method": "GET",
"path": "/selfService/resourcePack/{packId}",
"summary": "Retrieve a user resource pack",
"tag": "Self Service"
},
{
"method": "PUT",
"path": "/selfService/resourcePack/{packId}",
"summary": "Modify user resource pack",
"tag": "Self Service"
},
{
"method": "POST",
"path": "/selfService/resourcePack/byUserExtRelationId/{extRelationId}",
"summary": "Add a resource pack to a user",
"tag": "Self Service/External Relational ID"
},
{
"method": "DELETE",
"path": "/selfService/resourcePackServers/{packId}",
"summary": "Delete all servers attached to a pack ID",
"tag": "Self Service"
},
{
"method": "POST",
"path": "/selfService/resourcePackServers/{packId}/suspend",
"summary": "Suspend all servers assigned to a reosurce pack",
"tag": "Self Service"
},
{
"method": "POST",
"path": "/selfService/resourcePackServers/{packId}/unsuspend",
"summary": "Unsuspend all servers assigned to a reosurce pack",
"tag": "Self Service"
},
{
"method": "GET",
"path": "/selfService/usage/byUserExtRelationId/{extRelationId}",
"summary": "Retrieve a users usage",
"tag": "Self Service/External Relational ID"
},
{
"method": "GET",
"path": "/servers",
"summary": "Retrieve servers",
"tag": "Servers"
},
{
"method": "POST",
"path": "/servers",
"summary": "Create a server",
"tag": "Servers"
},
{
"method": "DELETE",
"path": "/servers/{serverId}",
"summary": "Delete a server",
"tag": "Servers"
},
{
"method": "GET",
"path": "/servers/{serverId}",
"summary": "Retrieve a server",
"tag": "Servers"
},
{
"method": "PUT",
"path": "/servers/{serverId}/backups/plan/{planId}",
"summary": "Add, remove or modify a backup plan",
"tag": "Servers"
},
{
"method": "POST",
"path": "/servers/{serverId}/build",
"summary": "Build a server",
"tag": "Servers"
},
{
"method": "POST",
"path": "/servers/{serverId}/customXML",
"summary": "Set custom XML",
"tag": "Servers"
},
{
"method": "GET",
"path": "/servers/{serverId}/firewall/{interface}",
"summary": "Retrieve firewall",
"tag": "Servers/Network/Firewall"
},
{
"method": "POST",
"path": "/servers/{serverId}/firewall/{interface}/disable",
"summary": "Disable firewall",
"tag": "Servers/Network/Firewall"
},
{
"method": "POST",
"path": "/servers/{serverId}/firewall/{interface}/enable",
"summary": "Enable firewall",
"tag": "Servers/Network/Firewall"
},
{
"method": "POST",
"path": "/servers/{serverId}/firewall/{interface}/rules",
"summary": "Apply firewall rulesets",
"tag": "Servers/Network/Firewall"
},
{
"method": "DELETE",
"path": "/servers/{serverId}/ipv4",
"summary": "Remove an array of IPv4 addresses",
"tag": "Servers/Network"
},
{
"method": "POST",
"path": "/servers/{serverId}/ipv4",
"summary": "Add an array of IPv4 addresses",
"tag": "Servers/Network"
},
{
"method": "POST",
"path": "/servers/{serverId}/ipv4Qty",
"summary": "Add a quantity of IPv4 addresses",
"tag": "Servers/Network"
},
{
"method": "PUT",
"path": "/servers/{serverId}/modify/cpuCores",
"summary": "Modify CPU cores",
"tag": "Servers"
},
{
"method": "PUT",
"path": "/servers/{serverId}/modify/cpuThrottle",
"summary": "Throttle a servers CPU",
"tag": "Servers"
},
{
"method": "PUT",
"path": "/servers/{serverId}/modify/memory",
"summary": "Modify memory",
"tag": "Servers"
},
{
"method": "PUT",
"path": "/servers/{serverId}/modify/name",
"summary": "Modify name",
"tag": "Servers"
},
{
"method": "PUT",
"path": "/servers/{serverId}/modify/traffic",
"summary": "Modify primary traffic allowance",
"tag": "Servers/Network/Traffic"
},
{
"method": "DELETE",
"path": "/servers/{serverId}/networkWhitelist",
"summary": "Remove an address from the whitelist",
"tag": "Servers/Network"
},
{
"method": "POST",
"path": "/servers/{serverId}/networkWhitelist",
"summary": "Add an address to the whitelist",
"tag": "Servers/Network"
},
{
"method": "PUT",
"path": "/servers/{serverId}/owner/{newOwnerId}",
"summary": "Change owner",
"tag": "Servers"
},
{
"method": "PUT",
"path": "/servers/{serverId}/package/{packageId}",
"summary": "Change a server package",
"tag": "Servers"
},
{
"method": "POST",
"path": "/servers/{serverId}/power/boot",
"summary": "Boot a server",
"tag": "Servers/Power"
},
{
"method": "POST",
"path": "/servers/{serverId}/power/poweroff",
"summary": "Poweroff a server",
"tag": "Servers/Power"
},
{
"method": "POST",
"path": "/servers/{serverId}/power/restart",
"summary": "Restart a server",
"tag": "Servers/Power"
},
{
"method": "POST",
"path": "/servers/{serverId}/power/shutdown",
"summary": "Shutdown a server",
"tag": "Servers/Power"
},
{
"method": "POST",
"path": "/servers/{serverId}/resetPassword",
"summary": "Reset a server password",
"tag": "Servers"
},
{
"method": "POST",
"path": "/servers/{serverId}/suspend",
"summary": "Suspend a server",
"tag": "Servers"
},
{
"method": "GET",
"path": "/servers/{serverId}/templates",
"summary": "Retrieve OS templates available to a server",
"tag": "Servers"
},
{
"method": "GET",
"path": "/servers/{serverId}/traffic",
"summary": "Retrieve a servers traffic statistics",
"tag": "Servers"
},
{
"method": "GET",
"path": "/servers/{serverId}/traffic/blocks",
"summary": "Retrieve a servers traffic blocks",
"tag": "Servers/Network/Traffic"
},
{
"method": "POST",
"path": "/servers/{serverId}/traffic/blocks",
"summary": "Add a traffic block to a server",
"tag": "Servers/Network/Traffic"
},
{
"method": "DELETE",
"path": "/servers/{serverId}/traffic/blocks/{blockId}",
"summary": "Remove a traffic block from a server",
"tag": "Servers/Network/Traffic"
},
{
"method": "POST",
"path": "/servers/{serverId}/unsuspend",
"summary": "Unsuspend a server",
"tag": "Servers"
},
{
"method": "GET",
"path": "/servers/{serverId}/vnc",
"summary": "Retrive VNC details",
"tag": "Servers"
},
{
"method": "POST",
"path": "/servers/{serverId}/vnc",
"summary": "Enable or disable VNC",
"tag": "Servers"
},
{
"method": "GET",
"path": "/servers/user/{userId}",
"summary": "Retrieve a users servers",
"tag": "Servers"
},
{
"method": "POST",
"path": "/ssh_keys",
"summary": "Add an SSH key to a user account",
"tag": "SSH Keys"
},
{
"method": "DELETE",
"path": "/ssh_keys/{keyId}",
"summary": "Delete an SSH key from a user",
"tag": "SSH Keys"
},
{
"method": "GET",
"path": "/ssh_keys/{keyId}",
"summary": "Retrieve an SSH key",
"tag": "SSH Keys"
},
{
"method": "GET",
"path": "/ssh_keys/user/{userId}",
"summary": "Retrieve a users SSH keys",
"tag": "SSH Keys"
},
{
"method": "POST",
"path": "/users",
"summary": "Create a user",
"tag": "Users"
},
{
"method": "POST",
"path": "/users/{extRelationId}/authenticationTokens",
"summary": "Generate a set of login tokens",
"tag": "Users/External Rel ID & Rel Str"
},
{
"method": "DELETE",
"path": "/users/{extRelationId}/byExtRelation",
"summary": "Delete a user",
"tag": "Users/External Rel ID & Rel Str"
},
{
"method": "GET",
"path": "/users/{extRelationId}/byExtRelation",
"summary": "Retrieve a user",
"tag": "Users/External Rel ID & Rel Str"
},
{
"method": "PUT",
"path": "/users/{extRelationId}/byExtRelation",
"summary": "Modify a user",
"tag": "Users/External Rel ID & Rel Str"
},
{
"method": "POST",
"path": "/users/{extRelationId}/byExtRelation/resetPassword",
"summary": "Change a user passowrd",
"tag": "Users/External Rel ID & Rel Str"
},
{
"method": "POST",
"path": "/users/{extRelationId}/serverAuthenticationTokens/{serverId}",
"summary": "Generate a set of loging tokens using a server ID",
"tag": "Users/External Rel ID & Rel Str"
}
]

8309
openapi.yaml Normal file

File diff suppressed because one or more lines are too long

1744
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "@ezscale/virtfusion-mcp",
"version": "1.0.0",
"description": "Model Context Protocol (MCP) server for the VirtFusion virtualization control panel API",
"type": "module",
"main": "dist/index.js",
"bin": {
"virtfusion-mcp": "dist/index.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts",
"extract-endpoints": "tsx scripts/extract-endpoints.ts",
"check-endpoint-drift": "tsx scripts/check-endpoint-drift.ts"
},
"keywords": [
"mcp",
"virtfusion",
"virtualization",
"hosting",
"model-context-protocol"
],
"author": "EZSCALE Hosting, LLC",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://git.ezscale.cloud/EZSCALE/virtfusion-mcp.git"
},
"engines": {
"node": ">=22.0.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"zod": "^3.25.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0",
"yaml": "^2.7.0"
}
}

View File

@@ -0,0 +1,79 @@
import { readFileSync } from 'node:fs';
import { parse } from 'yaml';
interface Endpoint {
method: string;
path: string;
summary: string;
tag: string;
}
function extractEndpoints(specPath: string): Endpoint[] {
const spec = parse(readFileSync(specPath, 'utf-8'));
const endpoints: Endpoint[] = [];
for (const [path, methods] of Object.entries(spec.paths as Record<string, Record<string, unknown>>)) {
for (const [method, details] of Object.entries(methods)) {
if (['get', 'post', 'put', 'delete', 'patch'].includes(method)) {
const op = details as { summary?: string; tags?: string[] };
endpoints.push({
method: method.toUpperCase(),
path,
summary: op.summary ?? '',
tag: op.tags?.[0] ?? 'Untagged',
});
}
}
}
return endpoints.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
}
function endpointKey(e: Endpoint): string {
return `${e.method} ${e.path}`;
}
const specPath = new URL('../openapi.yaml', import.meta.url).pathname;
const manifestPath = new URL('../endpoint-manifest.json', import.meta.url).pathname;
const current = extractEndpoints(specPath);
let manifest: Endpoint[];
try {
manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
} catch {
console.error('Error: endpoint-manifest.json not found. Run `npm run extract-endpoints > endpoint-manifest.json` first.');
process.exit(1);
}
const manifestKeys = new Set(manifest.map(endpointKey));
const currentKeys = new Set(current.map(endpointKey));
const added = current.filter((e) => !manifestKeys.has(endpointKey(e)));
const removed = manifest.filter((e) => !currentKeys.has(endpointKey(e)));
if (added.length === 0 && removed.length === 0) {
console.log(`No endpoint drift detected. ${current.length} endpoints match the manifest.`);
process.exit(0);
}
console.log('Endpoint drift detected!\n');
if (added.length > 0) {
console.log(`New endpoints (${added.length}):`);
for (const e of added) {
console.log(` + ${e.method} ${e.path}${e.summary} [${e.tag}]`);
}
console.log();
}
if (removed.length > 0) {
console.log(`Removed endpoints (${removed.length}):`);
for (const e of removed) {
console.log(` - ${e.method} ${e.path}${e.summary} [${e.tag}]`);
}
console.log();
}
console.log('Update endpoint-manifest.json: npm run extract-endpoints > endpoint-manifest.json');
process.exit(1);

View File

@@ -0,0 +1,32 @@
import { readFileSync } from 'node:fs';
import { parse } from 'yaml';
interface Endpoint {
method: string;
path: string;
summary: string;
tag: string;
}
const specPath = new URL('../openapi.yaml', import.meta.url).pathname;
const spec = parse(readFileSync(specPath, 'utf-8'));
const endpoints: Endpoint[] = [];
for (const [path, methods] of Object.entries(spec.paths as Record<string, Record<string, unknown>>)) {
for (const [method, details] of Object.entries(methods)) {
if (['get', 'post', 'put', 'delete', 'patch'].includes(method)) {
const op = details as { summary?: string; tags?: string[] };
endpoints.push({
method: method.toUpperCase(),
path,
summary: op.summary ?? '',
tag: op.tags?.[0] ?? 'Untagged',
});
}
}
}
endpoints.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
console.log(JSON.stringify(endpoints, null, 2));

76
src/client.ts Normal file
View File

@@ -0,0 +1,76 @@
import { VirtFusionApiError } from './errors.js';
import { QueryParams } from './types.js';
export class VirtFusionClient {
private readonly baseUrl: string;
private readonly token: string;
constructor(baseUrl: string, token: string) {
this.baseUrl = baseUrl.replace(/\/+$/, '');
this.token = token;
}
private buildUrl(path: string, query?: QueryParams): string {
const url = new URL(`${this.baseUrl}${path}`);
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== undefined) {
url.searchParams.set(key, String(value));
}
}
}
return url.toString();
}
private async request(method: string, path: string, body?: unknown, query?: QueryParams): Promise<unknown> {
const url = this.buildUrl(path, query);
const headers: Record<string, string> = {
Authorization: `Bearer ${this.token}`,
Accept: 'application/json',
};
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
}
const response = await fetch(url, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
});
if (response.status === 204) {
return { success: true };
}
let responseBody: unknown;
const text = await response.text();
try {
responseBody = JSON.parse(text);
} catch {
responseBody = text;
}
if (!response.ok) {
throw new VirtFusionApiError(response.status, response.statusText, responseBody);
}
return responseBody;
}
async get(path: string, query?: QueryParams): Promise<unknown> {
return this.request('GET', path, undefined, query);
}
async post(path: string, body?: unknown, query?: QueryParams): Promise<unknown> {
return this.request('POST', path, body, query);
}
async put(path: string, body?: unknown, query?: QueryParams): Promise<unknown> {
return this.request('PUT', path, body, query);
}
async delete(path: string, body?: unknown, query?: QueryParams): Promise<unknown> {
return this.request('DELETE', path, body, query);
}
}

41
src/errors.ts Normal file
View File

@@ -0,0 +1,41 @@
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
export class VirtFusionApiError extends Error {
constructor(
public readonly statusCode: number,
public readonly statusText: string,
public readonly errorBody: unknown,
) {
super(`VirtFusion API error ${statusCode}: ${statusText}`);
this.name = 'VirtFusionApiError';
}
}
export function formatErrorResponse(error: unknown): CallToolResult {
if (error instanceof VirtFusionApiError) {
return {
isError: true,
content: [
{
type: 'text',
text: JSON.stringify(
{
error: true,
statusCode: error.statusCode,
statusText: error.statusText,
details: error.errorBody,
},
null,
2,
),
},
],
};
}
const message = error instanceof Error ? error.message : String(error);
return {
isError: true,
content: [{ type: 'text', text: JSON.stringify({ error: true, message }, null, 2) }],
};
}

40
src/index.ts Normal file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { VirtFusionClient } from './client.js';
import { registerAllTools } from './tools/index.js';
const VIRTFUSION_API_URL = process.env.VIRTFUSION_API_URL;
const VIRTFUSION_API_TOKEN = process.env.VIRTFUSION_API_TOKEN;
if (!VIRTFUSION_API_URL) {
console.error('Error: VIRTFUSION_API_URL environment variable is required.');
console.error('Set it to your VirtFusion panel URL, e.g. https://cp.example.com/api/v1');
process.exit(1);
}
if (!VIRTFUSION_API_TOKEN) {
console.error('Error: VIRTFUSION_API_TOKEN environment variable is required.');
console.error('Generate an API token in your VirtFusion admin panel.');
process.exit(1);
}
const client = new VirtFusionClient(VIRTFUSION_API_URL, VIRTFUSION_API_TOKEN);
const server = new McpServer({
name: 'virtfusion-mcp',
version: '1.0.0',
});
registerAllTools(server, client);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main().catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});

22
src/tools/backups.ts Normal file
View File

@@ -0,0 +1,22 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerBackupTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'backups_list_by_server',
'List all backups for a specific server',
{
serverId: z.number().describe('The server ID'),
},
async ({ serverId }) => {
try {
const result = await client.get(`/backups/server/${serverId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

22
src/tools/dns.ts Normal file
View File

@@ -0,0 +1,22 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerDnsTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'dns_get_service',
'Retrieve a DNS service',
{
serviceId: z.number().describe('The DNS service ID'),
},
async ({ serviceId }) => {
try {
const result = await client.get(`/dns/services/${serviceId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

20
src/tools/general.ts Normal file
View File

@@ -0,0 +1,20 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerGeneralTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'general_test_connection',
'Test the API connection to VirtFusion',
{},
async () => {
try {
const result = await client.get('/connect');
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

View File

@@ -0,0 +1,54 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerHypervisorGroupTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'hypervisor_groups_list',
'List all hypervisor groups',
{
results: z.number().optional().describe('Number of results to return'),
},
async ({ results }) => {
try {
const result = await client.get('/compute/hypervisors/groups', { results });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'hypervisor_groups_get',
'Retrieve details of a specific hypervisor group',
{
hypervisorGroupId: z.number().describe('The hypervisor group ID'),
},
async ({ hypervisorGroupId }) => {
try {
const result = await client.get(`/compute/hypervisors/groups/${hypervisorGroupId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'hypervisor_groups_get_resources',
'Retrieve resources for a hypervisor group',
{
hypervisorGroupId: z.number().describe('The hypervisor group ID'),
},
async ({ hypervisorGroupId }) => {
try {
const result = await client.get(`/compute/hypervisors/groups/${hypervisorGroupId}/resources`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

38
src/tools/hypervisors.ts Normal file
View File

@@ -0,0 +1,38 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerHypervisorTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'hypervisors_list',
'List all hypervisors',
{
results: z.number().optional().describe('Number of results to return'),
},
async ({ results }) => {
try {
const result = await client.get('/compute/hypervisors', { results });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'hypervisors_get',
'Retrieve details of a specific hypervisor',
{
hypervisorId: z.number().describe('The hypervisor ID'),
},
async ({ hypervisorId }) => {
try {
const result = await client.get(`/compute/hypervisors/${hypervisorId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

39
src/tools/index.ts Normal file
View File

@@ -0,0 +1,39 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { VirtFusionClient } from '../client.js';
import { registerGeneralTools } from './general.js';
import { registerHypervisorTools } from './hypervisors.js';
import { registerHypervisorGroupTools } from './hypervisor-groups.js';
import { registerServerTools } from './servers.js';
import { registerServerPowerTools } from './servers-power.js';
import { registerServerNetworkTools } from './servers-network.js';
import { registerServerFirewallTools } from './servers-firewall.js';
import { registerServerTrafficTools } from './servers-traffic.js';
import { registerIpBlockTools } from './ip-blocks.js';
import { registerBackupTools } from './backups.js';
import { registerDnsTools } from './dns.js';
import { registerMediaTools } from './media.js';
import { registerPackageTools } from './packages.js';
import { registerQueueTools } from './queue.js';
import { registerSshKeyTools } from './ssh-keys.js';
import { registerUserTools } from './users.js';
import { registerSelfServiceTools } from './self-service.js';
export function registerAllTools(server: McpServer, client: VirtFusionClient): void {
registerGeneralTools(server, client);
registerHypervisorTools(server, client);
registerHypervisorGroupTools(server, client);
registerServerTools(server, client);
registerServerPowerTools(server, client);
registerServerNetworkTools(server, client);
registerServerFirewallTools(server, client);
registerServerTrafficTools(server, client);
registerIpBlockTools(server, client);
registerBackupTools(server, client);
registerDnsTools(server, client);
registerMediaTools(server, client);
registerPackageTools(server, client);
registerQueueTools(server, client);
registerSshKeyTools(server, client);
registerUserTools(server, client);
registerSelfServiceTools(server, client);
}

57
src/tools/ip-blocks.ts Normal file
View File

@@ -0,0 +1,57 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerIpBlockTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'ip_blocks_list',
'List all IP blocks',
{
results: z.number().optional().describe('Number of results to return'),
},
async ({ results }) => {
try {
const result = await client.get('/connectivity/ipblocks', { results });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'ip_blocks_get',
'Retrieve details of a specific IP block',
{
blockId: z.number().describe('The IP block ID'),
},
async ({ blockId }) => {
try {
const result = await client.get(`/connectivity/ipblocks/${blockId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'ip_blocks_add_ipv4_range',
'Add an IPv4 range to an IP block',
{
blockId: z.number().describe('The IP block ID'),
type: z.string().describe('Type of IP range (always "range")'),
start: z.string().describe('Start IPv4 address'),
end: z.string().describe('End IPv4 address'),
},
async ({ blockId, type, start, end }) => {
try {
const result = await client.post(`/connectivity/ipblocks/${blockId}/ipv4`, { type, start, end });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

38
src/tools/media.ts Normal file
View File

@@ -0,0 +1,38 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerMediaTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'media_get_iso',
'Retrieve details of a specific ISO image',
{
isoId: z.number().describe('The ISO image ID'),
},
async ({ isoId }) => {
try {
const result = await client.get(`/media/iso/${isoId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'media_get_templates_by_package',
'List OS templates available for a specific package',
{
serverPackageId: z.number().describe('The server package ID'),
},
async ({ serverPackageId }) => {
try {
const result = await client.get(`/media/templates/fromServerPackageSpec/${serverPackageId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

36
src/tools/packages.ts Normal file
View File

@@ -0,0 +1,36 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerPackageTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'packages_list',
'List all available packages',
{},
async () => {
try {
const result = await client.get('/packages');
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'packages_get',
'Retrieve details of a specific package',
{
packageId: z.number().describe('The package ID'),
},
async ({ packageId }) => {
try {
const result = await client.get(`/packages/${packageId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

22
src/tools/queue.ts Normal file
View File

@@ -0,0 +1,22 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerQueueTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'queue_get',
'Retrieve the status of a queue item',
{
queueId: z.number().describe('The queue item ID'),
},
async ({ queueId }) => {
try {
const result = await client.get(`/queue/${queueId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

348
src/tools/self-service.ts Normal file
View File

@@ -0,0 +1,348 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerSelfServiceTools(server: McpServer, client: VirtFusionClient): void {
// --- Self Service / External Relational ID endpoints ---
server.tool(
'self_service_add_credit',
'Add credit to a user',
{
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
tokens: z.number().describe('Number of credit tokens to add'),
reference_1: z.number().optional().describe('Optional numeric reference'),
reference_2: z.string().optional().describe('Optional string reference'),
},
async ({ extRelationId, relStr, tokens, reference_1, reference_2 }) => {
try {
const body: Record<string, unknown> = { tokens };
if (reference_1 !== undefined) body.reference_1 = reference_1;
if (reference_2 !== undefined) body.reference_2 = reference_2;
const result = await client.post(`/selfService/credit/byUserExtRelationId/${extRelationId}`, body, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_cancel_credit',
'Cancel credit that was applied to a user',
{
creditId: z.number().describe('The credit ID to cancel'),
},
async ({ creditId }) => {
try {
const result = await client.delete(`/selfService/credit/${creditId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_get_currencies',
'Retrieve available currencies',
{},
async () => {
try {
const result = await client.get('/selfService/currencies');
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_add_hourly_group_profile',
'Add an hourly group profile to a user',
{
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
profileId: z.number().describe('The hourly group profile ID'),
},
async ({ extRelationId, relStr, profileId }) => {
try {
const result = await client.post(`/selfService/hourlyGroupProfile/byUserExtRelationId/${extRelationId}`, { profileId }, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_remove_hourly_group_profile',
'Remove an hourly group profile from a user',
{
profileId: z.number().describe('The hourly group profile ID'),
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
},
async ({ profileId, extRelationId, relStr }) => {
try {
const result = await client.delete(`/selfService/hourlyGroupProfile/${profileId}/byUserExtRelationId/${extRelationId}`, undefined, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_add_resource_group_profile',
'Add a resource group profile to a user',
{
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
profileId: z.number().describe('The resource group profile ID'),
},
async ({ extRelationId, relStr, profileId }) => {
try {
const result = await client.post(`/selfService/resourceGroupProfile/byUserExtRelationId/${extRelationId}`, { profileId }, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_remove_resource_group_profile',
'Remove a resource group profile from a user',
{
profileId: z.number().describe('The resource group profile ID'),
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
},
async ({ profileId, extRelationId, relStr }) => {
try {
const result = await client.delete(`/selfService/resourceGroupProfile/${profileId}/byUserExtRelationId/${extRelationId}`, undefined, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_add_resource_pack',
'Add a resource pack to a user',
{
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
packId: z.number().describe('The resource pack ID'),
enabled: z.boolean().optional().describe('Whether the resource pack is enabled'),
},
async ({ extRelationId, relStr, packId, enabled }) => {
try {
const body: Record<string, unknown> = { packId };
if (enabled !== undefined) body.enabled = enabled;
const result = await client.post(`/selfService/resourcePack/byUserExtRelationId/${extRelationId}`, body, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
// --- Self Service endpoints (non-relational) ---
server.tool(
'self_service_get_resource_pack',
'Retrieve a user resource pack',
{
packId: z.number().describe('The resource pack ID'),
},
async ({ packId }) => {
try {
const result = await client.get(`/selfService/resourcePack/${packId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_modify_resource_pack',
'Modify a user resource pack',
{
packId: z.number().describe('The resource pack ID'),
},
async ({ packId }) => {
try {
const result = await client.put(`/selfService/resourcePack/${packId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_delete_resource_pack',
'Delete a user resource pack',
{
packId: z.number().describe('The resource pack ID'),
},
async ({ packId }) => {
try {
const result = await client.delete(`/selfService/resourcePack/${packId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_get_hourly_stats',
'Retrieve hourly statistics for a user',
{
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
period: z.string().optional().describe('Comma-separated dates in YYYY-MM-DD format'),
range: z.string().optional().describe('Date range filter'),
},
async ({ extRelationId, relStr, period, range }) => {
try {
const result = await client.get(`/selfService/hourlyStats/byUserExtRelationId/${extRelationId}`, { relStr, period, range });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_set_hourly_resource_pack',
'Set an hourly resource pack for a user',
{
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
packId: z.number().describe('The resource pack ID'),
},
async ({ extRelationId, relStr, packId }) => {
try {
const result = await client.put(`/selfService/hourlyResourcePack/byUserExtRelationId/${extRelationId}`, { packId }, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_modify_access',
"Modify a user's self-service access settings",
{
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
selfService: z.number().optional().describe('Self-service level (0-3)'),
selfServiceHourlyCredit: z.boolean().optional().describe('Enable hourly credit for self-service'),
},
async ({ extRelationId, relStr, selfService, selfServiceHourlyCredit }) => {
try {
const body: Record<string, unknown> = {};
if (selfService !== undefined) body.selfService = selfService;
if (selfServiceHourlyCredit !== undefined) body.selfServiceHourlyCredit = selfServiceHourlyCredit;
const result = await client.put(`/selfService/access/byUserExtRelationId/${extRelationId}`, Object.keys(body).length > 0 ? body : undefined, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_get_report',
'Generate a report for a user',
{
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
},
async ({ extRelationId, relStr }) => {
try {
const result = await client.get(`/selfService/report/byUserExtRelationId/${extRelationId}`, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_get_usage',
'Retrieve usage data for a user',
{
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
},
async ({ extRelationId, relStr }) => {
try {
const result = await client.get(`/selfService/usage/byUserExtRelationId/${extRelationId}`, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_delete_pack_servers',
'Delete all servers attached to a resource pack',
{
packId: z.number().describe('The resource pack ID'),
},
async ({ packId }) => {
try {
const result = await client.delete(`/selfService/resourcePackServers/${packId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_suspend_pack_servers',
'Suspend all servers assigned to a resource pack',
{
packId: z.number().describe('The resource pack ID'),
},
async ({ packId }) => {
try {
const result = await client.post(`/selfService/resourcePackServers/${packId}/suspend`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'self_service_unsuspend_pack_servers',
'Unsuspend all servers assigned to a resource pack',
{
packId: z.number().describe('The resource pack ID'),
},
async ({ packId }) => {
try {
const result = await client.post(`/selfService/resourcePackServers/${packId}/unsuspend`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

View File

@@ -0,0 +1,79 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerServerFirewallTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'servers_firewall_get',
'Retrieve firewall configuration for a server interface',
{
serverId: z.number().describe('The server ID'),
interface: z.enum(['primary', 'secondary']).describe('Network interface'),
sync: z.boolean().optional().describe('Sync firewall state from hypervisor'),
},
async ({ serverId, interface: iface, sync }) => {
try {
const result = await client.get(`/servers/${serverId}/firewall/${iface}`, { sync });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_firewall_enable',
'Enable the firewall on a server interface',
{
serverId: z.number().describe('The server ID'),
interface: z.enum(['primary', 'secondary']).describe('Network interface'),
sync: z.boolean().optional().describe('Sync firewall state from hypervisor'),
},
async ({ serverId, interface: iface, sync }) => {
try {
const result = await client.post(`/servers/${serverId}/firewall/${iface}/enable`, undefined, { sync });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_firewall_disable',
'Disable the firewall on a server interface',
{
serverId: z.number().describe('The server ID'),
interface: z.enum(['primary', 'secondary']).describe('Network interface'),
sync: z.boolean().optional().describe('Sync firewall state from hypervisor'),
},
async ({ serverId, interface: iface, sync }) => {
try {
const result = await client.post(`/servers/${serverId}/firewall/${iface}/disable`, undefined, { sync });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_firewall_set_rules',
'Apply firewall rulesets to a server interface',
{
serverId: z.number().describe('The server ID'),
interface: z.enum(['primary', 'secondary']).describe('Network interface'),
sync: z.boolean().optional().describe('Sync firewall state from hypervisor'),
rulesets: z.array(z.number()).describe('Firewall ruleset IDs (empty array to flush all rules)'),
},
async ({ serverId, interface: iface, sync, rulesets }) => {
try {
const result = await client.post(`/servers/${serverId}/firewall/${iface}/rules`, { rulesets }, { sync });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

View File

@@ -0,0 +1,105 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerServerNetworkTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'servers_network_whitelist_add',
"Add an IP address to a server's network whitelist",
{
serverId: z.number().describe('The server ID'),
interface: z.enum(['primary', 'secondary']).describe('Network interface'),
ip: z.string().describe('IP address to whitelist'),
cidr: z.number().describe('CIDR notation prefix length'),
},
async ({ serverId, interface: iface, ip, cidr }) => {
try {
const result = await client.post(`/servers/${serverId}/networkWhitelist`, {
interface: iface,
ip,
cidr,
});
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_network_whitelist_remove',
"Remove an IP from a server's network whitelist",
{
serverId: z.number().describe('The server ID'),
interface: z.string().describe('Network interface'),
ip: z.string().describe('IP address to remove'),
},
async ({ serverId, interface: iface, ip }) => {
try {
const result = await client.delete(`/servers/${serverId}/networkWhitelist`, {
interface: iface,
ip,
});
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_network_ipv4_add',
'Add specific IPv4 addresses to a server',
{
serverId: z.number().describe('The server ID'),
ip: z.array(z.string()).describe('Array of IPv4 addresses to add'),
},
async ({ serverId, ip }) => {
try {
const result = await client.post(`/servers/${serverId}/ipv4`, { ip });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_network_ipv4_remove',
'Remove IPv4 addresses from a server',
{
serverId: z.number().describe('The server ID'),
ip: z.array(z.string()).describe('Array of IPv4 addresses to remove'),
},
async ({ serverId, ip }) => {
try {
const result = await client.delete(`/servers/${serverId}/ipv4`, { ip });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_network_ipv4_add_qty',
'Add a quantity of IPv4 addresses to a server',
{
serverId: z.number().describe('The server ID'),
interface: z.string().describe('Network interface'),
quantity: z.number().describe('Number of IPv4 addresses to add'),
},
async ({ serverId, interface: iface, quantity }) => {
try {
const result = await client.post(`/servers/${serverId}/ipv4Qty`, {
interface: iface,
quantity,
});
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

View File

@@ -0,0 +1,70 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerServerPowerTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'servers_power_boot',
'Boot a server',
{
serverId: z.number().describe('The server ID'),
},
async ({ serverId }) => {
try {
const result = await client.post(`/servers/${serverId}/power/boot`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_power_shutdown',
'Gracefully shutdown a server',
{
serverId: z.number().describe('The server ID'),
},
async ({ serverId }) => {
try {
const result = await client.post(`/servers/${serverId}/power/shutdown`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_power_restart',
'Restart a server',
{
serverId: z.number().describe('The server ID'),
},
async ({ serverId }) => {
try {
const result = await client.post(`/servers/${serverId}/power/restart`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_power_poweroff',
'Force power off a server',
{
serverId: z.number().describe('The server ID'),
},
async ({ serverId }) => {
try {
const result = await client.post(`/servers/${serverId}/power/poweroff`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

View File

@@ -0,0 +1,74 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerServerTrafficTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'servers_traffic_list_blocks',
'List traffic blocks for a server',
{
serverId: z.number().describe('The server ID'),
},
async ({ serverId }) => {
try {
const result = await client.get(`/servers/${serverId}/traffic/blocks`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_traffic_add_block',
'Add a traffic block to a server',
{
serverId: z.number().describe('The server ID'),
month: z.number().describe('Month number for the traffic block'),
amount: z.number().describe('Traffic amount in GB'),
},
async ({ serverId, month, amount }) => {
try {
const result = await client.post(`/servers/${serverId}/traffic/blocks`, { month, amount });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_traffic_remove_block',
'Remove a traffic block from a server',
{
serverId: z.number().describe('The server ID'),
blockId: z.number().describe('The traffic block ID'),
},
async ({ serverId, blockId }) => {
try {
const result = await client.delete(`/servers/${serverId}/traffic/blocks/${blockId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_traffic_modify',
"Modify a server's primary traffic allowance",
{
serverId: z.number().describe('The server ID'),
traffic: z.string().describe('Traffic allowance (0-999999999)'),
},
async ({ serverId, traffic }) => {
try {
const result = await client.put(`/servers/${serverId}/modify/traffic`, { traffic });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

444
src/tools/servers.ts Normal file
View File

@@ -0,0 +1,444 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerServerTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'servers_list',
'List all servers with optional filtering',
{
type: z.string().optional().describe('Filter by server type'),
results: z.number().optional().describe('Number of results to return'),
hypervisorId: z.number().optional().describe('Filter by hypervisor ID'),
},
async ({ type, results, hypervisorId }) => {
try {
const result = await client.get('/servers', { type, results, hypervisorId });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_create',
'Create a new server',
{
dryRun: z.boolean().optional().describe('Perform a dry run without creating the server'),
packageId: z.number().describe('Package ID for the server'),
userId: z.number().describe('User ID who will own the server'),
hypervisorId: z.number().describe('Hypervisor ID to deploy on'),
ipv4: z.number().optional().describe('Number of IPv4 addresses'),
storage: z.number().optional().describe('Storage in GB'),
traffic: z.number().optional().describe('Traffic allowance'),
memory: z.number().optional().describe('Memory in MB'),
cpuCores: z.number().optional().describe('Number of CPU cores'),
networkSpeedInbound: z.number().optional().describe('Inbound network speed'),
networkSpeedOutbound: z.number().optional().describe('Outbound network speed'),
storageProfile: z.number().optional().describe('Storage profile ID'),
networkProfile: z.number().optional().describe('Network profile ID'),
firewallRulesets: z.array(z.number()).optional().describe('Firewall ruleset IDs'),
hypervisorAssetGroups: z.array(z.number()).optional().describe('Hypervisor asset group IDs'),
additionalStorage1Enable: z.boolean().optional().describe('Enable additional storage 1'),
additionalStorage2Enable: z.boolean().optional().describe('Enable additional storage 2'),
additionalStorage1Profile: z.number().optional().describe('Storage profile for additional storage 1'),
additionalStorage2Profile: z.number().optional().describe('Storage profile for additional storage 2'),
additionalStorage1Capacity: z.number().optional().describe('Capacity for additional storage 1'),
additionalStorage2Capacity: z.number().optional().describe('Capacity for additional storage 2'),
},
async ({ dryRun, packageId, userId, hypervisorId, ipv4, storage, traffic, memory, cpuCores, networkSpeedInbound, networkSpeedOutbound, storageProfile, networkProfile, firewallRulesets, hypervisorAssetGroups, additionalStorage1Enable, additionalStorage2Enable, additionalStorage1Profile, additionalStorage2Profile, additionalStorage1Capacity, additionalStorage2Capacity }) => {
try {
const body: Record<string, unknown> = {
packageId,
userId,
hypervisorId,
};
if (ipv4 !== undefined) body.ipv4 = ipv4;
if (storage !== undefined) body.storage = storage;
if (traffic !== undefined) body.traffic = traffic;
if (memory !== undefined) body.memory = memory;
if (cpuCores !== undefined) body.cpuCores = cpuCores;
if (networkSpeedInbound !== undefined) body.networkSpeedInbound = networkSpeedInbound;
if (networkSpeedOutbound !== undefined) body.networkSpeedOutbound = networkSpeedOutbound;
if (storageProfile !== undefined) body.storageProfile = storageProfile;
if (networkProfile !== undefined) body.networkProfile = networkProfile;
if (firewallRulesets !== undefined) body.firewallRulesets = firewallRulesets;
if (hypervisorAssetGroups !== undefined) body.hypervisorAssetGroups = hypervisorAssetGroups;
if (additionalStorage1Enable !== undefined) body.additionalStorage1Enable = additionalStorage1Enable;
if (additionalStorage2Enable !== undefined) body.additionalStorage2Enable = additionalStorage2Enable;
if (additionalStorage1Profile !== undefined) body.additionalStorage1Profile = additionalStorage1Profile;
if (additionalStorage2Profile !== undefined) body.additionalStorage2Profile = additionalStorage2Profile;
if (additionalStorage1Capacity !== undefined) body.additionalStorage1Capacity = additionalStorage1Capacity;
if (additionalStorage2Capacity !== undefined) body.additionalStorage2Capacity = additionalStorage2Capacity;
const result = await client.post('/servers', body, { dryRun });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_get',
'Retrieve details of a specific server',
{
serverId: z.number().describe('The server ID'),
remoteState: z.boolean().optional().describe('Include remote state information'),
},
async ({ serverId, remoteState }) => {
try {
const result = await client.get(`/servers/${serverId}`, { remoteState });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_delete',
'Delete a server',
{
serverId: z.number().describe('The server ID'),
delay: z.number().optional().describe('Delay in seconds before deletion'),
},
async ({ serverId, delay }) => {
try {
const result = await client.delete(`/servers/${serverId}`, undefined, { delay });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_build',
'Build/rebuild a server with an OS template',
{
serverId: z.number().describe('The server ID'),
operatingSystemId: z.number().describe('Operating system template ID'),
name: z.string().optional().describe('Server name'),
hostname: z.string().optional().describe('Server hostname'),
sshKeys: z.array(z.number()).optional().describe('SSH key IDs to add'),
vnc: z.boolean().optional().describe('Enable VNC'),
ipv6: z.boolean().optional().describe('Enable IPv6'),
email: z.boolean().optional().describe('Send email notification'),
swap: z.number().optional().describe('Swap size in MB'),
},
async ({ serverId, operatingSystemId, name, hostname, sshKeys, vnc, ipv6, email, swap }) => {
try {
const body: Record<string, unknown> = { operatingSystemId };
if (name !== undefined) body.name = name;
if (hostname !== undefined) body.hostname = hostname;
if (sshKeys !== undefined) body.sshKeys = sshKeys;
if (vnc !== undefined) body.vnc = vnc;
if (ipv6 !== undefined) body.ipv6 = ipv6;
if (email !== undefined) body.email = email;
if (swap !== undefined) body.swap = swap;
const result = await client.post(`/servers/${serverId}/build`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_list_templates',
'List OS templates available for a server',
{
serverId: z.number().describe('The server ID'),
},
async ({ serverId }) => {
try {
const result = await client.get(`/servers/${serverId}/templates`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_modify_name',
"Change a server's display name",
{
serverId: z.number().describe('The server ID'),
name: z.string().describe('New display name for the server'),
},
async ({ serverId, name }) => {
try {
const result = await client.put(`/servers/${serverId}/modify/name`, { name });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_modify_cpu',
'Modify the number of CPU cores for a server',
{
serverId: z.number().describe('The server ID'),
cpuCores: z.number().describe('Number of CPU cores'),
},
async ({ serverId, cpuCores }) => {
try {
const result = await client.put(`/servers/${serverId}/modify/cpuCores`, { cpuCores });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_modify_memory',
'Modify the memory allocation for a server in MB',
{
serverId: z.number().describe('The server ID'),
memory: z.number().describe('Memory in MB'),
},
async ({ serverId, memory }) => {
try {
const result = await client.put(`/servers/${serverId}/modify/memory`, { memory });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_throttle_cpu',
"Throttle a server's CPU",
{
serverId: z.number().describe('The server ID'),
cpuThrottle: z.number().describe('CPU throttle value'),
},
async ({ serverId, cpuThrottle }) => {
try {
const result = await client.put(`/servers/${serverId}/modify/cpuThrottle`, { cpuThrottle });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_get_traffic',
'Retrieve traffic statistics for a server',
{
serverId: z.number().describe('The server ID'),
},
async ({ serverId }) => {
try {
const result = await client.get(`/servers/${serverId}/traffic`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_suspend',
'Suspend a server',
{
serverId: z.number().describe('The server ID'),
},
async ({ serverId }) => {
try {
const result = await client.post(`/servers/${serverId}/suspend`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_unsuspend',
'Unsuspend a server',
{
serverId: z.number().describe('The server ID'),
},
async ({ serverId }) => {
try {
const result = await client.post(`/servers/${serverId}/unsuspend`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_reset_password',
"Reset a server's root/admin password",
{
serverId: z.number().describe('The server ID'),
user: z.enum(['root', 'Administrator']).describe('User to reset password for'),
sendMail: z.boolean().optional().describe('Send password via email'),
},
async ({ serverId, user, sendMail }) => {
try {
const body: Record<string, unknown> = { user };
if (sendMail !== undefined) body.sendMail = sendMail;
const result = await client.post(`/servers/${serverId}/resetPassword`, body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_enable_vnc',
'Enable or disable VNC for a server',
{
serverId: z.number().describe('The server ID'),
vnc: z.boolean().describe('Enable or disable VNC'),
},
async ({ serverId, vnc }) => {
try {
const result = await client.post(`/servers/${serverId}/vnc`, { vnc });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_get_vnc',
'Retrieve VNC connection details for a server',
{
serverId: z.number().describe('The server ID'),
},
async ({ serverId }) => {
try {
const result = await client.get(`/servers/${serverId}/vnc`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_set_custom_xml',
'Set custom XML configuration for a server',
{
serverId: z.number().describe('The server ID'),
xml: z.string().describe('Custom XML configuration'),
},
async ({ serverId, xml }) => {
try {
const result = await client.post(`/servers/${serverId}/customXML`, { xml });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_change_owner',
'Change the owner of a server',
{
serverId: z.number().describe('The server ID'),
newOwnerId: z.number().describe('New owner user ID'),
},
async ({ serverId, newOwnerId }) => {
try {
const result = await client.put(`/servers/${serverId}/owner/${newOwnerId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_change_package',
"Change a server's package with optional resource sync flags",
{
serverId: z.number().describe('The server ID'),
packageId: z.number().describe('New package ID'),
backupPlan: z.boolean().optional().describe('Sync backup plan'),
cpu: z.boolean().optional().describe('Sync CPU'),
memory: z.boolean().optional().describe('Sync memory'),
primaryDiskReadIOPS: z.boolean().optional().describe('Sync primary disk read IOPS'),
primaryDiskReadThroughput: z.boolean().optional().describe('Sync primary disk read throughput'),
primaryDiskSize: z.boolean().optional().describe('Sync primary disk size'),
primaryDiskWriteIOPS: z.boolean().optional().describe('Sync primary disk write IOPS'),
primaryDiskWriteThroughput: z.boolean().optional().describe('Sync primary disk write throughput'),
primaryNetworkInboundSpeed: z.boolean().optional().describe('Sync primary network inbound speed'),
primaryNetworkOutboundSpeed: z.boolean().optional().describe('Sync primary network outbound speed'),
primaryNetworkTraffic: z.boolean().optional().describe('Sync primary network traffic'),
},
async ({ serverId, packageId, backupPlan, cpu, memory, primaryDiskReadIOPS, primaryDiskReadThroughput, primaryDiskSize, primaryDiskWriteIOPS, primaryDiskWriteThroughput, primaryNetworkInboundSpeed, primaryNetworkOutboundSpeed, primaryNetworkTraffic }) => {
try {
const body: Record<string, unknown> = {};
if (backupPlan !== undefined) body.backupPlan = backupPlan;
if (cpu !== undefined) body.cpu = cpu;
if (memory !== undefined) body.memory = memory;
if (primaryDiskReadIOPS !== undefined) body.primaryDiskReadIOPS = primaryDiskReadIOPS;
if (primaryDiskReadThroughput !== undefined) body.primaryDiskReadThroughput = primaryDiskReadThroughput;
if (primaryDiskSize !== undefined) body.primaryDiskSize = primaryDiskSize;
if (primaryDiskWriteIOPS !== undefined) body.primaryDiskWriteIOPS = primaryDiskWriteIOPS;
if (primaryDiskWriteThroughput !== undefined) body.primaryDiskWriteThroughput = primaryDiskWriteThroughput;
if (primaryNetworkInboundSpeed !== undefined) body.primaryNetworkInboundSpeed = primaryNetworkInboundSpeed;
if (primaryNetworkOutboundSpeed !== undefined) body.primaryNetworkOutboundSpeed = primaryNetworkOutboundSpeed;
if (primaryNetworkTraffic !== undefined) body.primaryNetworkTraffic = primaryNetworkTraffic;
const result = await client.put(`/servers/${serverId}/package/${packageId}`, Object.keys(body).length > 0 ? body : undefined);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_set_backup_plan',
"Add, remove or modify a server's backup plan",
{
serverId: z.number().describe('The server ID'),
planId: z.number().describe('Backup plan ID (0 to remove)'),
},
async ({ serverId, planId }) => {
try {
const result = await client.put(`/servers/${serverId}/backups/plan/${planId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'servers_list_by_user',
'List all servers owned by a specific user',
{
userId: z.number().describe('The user ID'),
},
async ({ userId }) => {
try {
const result = await client.get(`/servers/user/${userId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

72
src/tools/ssh-keys.ts Normal file
View File

@@ -0,0 +1,72 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerSshKeyTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'ssh_keys_create',
'Add an SSH key to a user account',
{
userId: z.number().describe('The user ID'),
name: z.string().describe('Name for the SSH key'),
publicKey: z.string().describe('The public key content'),
},
async ({ userId, name, publicKey }) => {
try {
const result = await client.post('/ssh_keys', { userId, name, publicKey });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'ssh_keys_get',
'Retrieve details of a specific SSH key',
{
keyId: z.number().describe('The SSH key ID'),
},
async ({ keyId }) => {
try {
const result = await client.get(`/ssh_keys/${keyId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'ssh_keys_delete',
'Delete an SSH key',
{
keyId: z.number().describe('The SSH key ID'),
},
async ({ keyId }) => {
try {
const result = await client.delete(`/ssh_keys/${keyId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'ssh_keys_list_by_user',
'List all SSH keys for a specific user',
{
userId: z.number().describe('The user ID'),
},
async ({ userId }) => {
try {
const result = await client.get(`/ssh_keys/user/${userId}`);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

162
src/tools/users.ts Normal file
View File

@@ -0,0 +1,162 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { VirtFusionClient } from '../client.js';
import { formatErrorResponse } from '../errors.js';
export function registerUserTools(server: McpServer, client: VirtFusionClient): void {
server.tool(
'users_create',
'Create a new user',
{
name: z.string().describe('User display name'),
email: z.string().describe('User email address'),
extRelationId: z.number().optional().describe('External relation ID'),
relStr: z.string().optional().describe('Relation string'),
selfService: z.number().optional().describe('Self-service level (0-3)'),
selfServiceHourlyCredit: z.boolean().optional().describe('Enable hourly credit for self-service'),
selfServiceHourlyGroupProfiles: z.array(z.number()).optional().describe('Hourly group profile IDs'),
selfServiceResourceGroupProfiles: z.array(z.number()).optional().describe('Resource group profile IDs'),
selfServiceHourlyResourcePack: z.number().optional().describe('Hourly resource pack ID'),
sendMail: z.boolean().optional().describe('Send welcome email'),
},
async ({ name, email, extRelationId, relStr, selfService, selfServiceHourlyCredit, selfServiceHourlyGroupProfiles, selfServiceResourceGroupProfiles, selfServiceHourlyResourcePack, sendMail }) => {
try {
const body: Record<string, unknown> = { name, email };
if (extRelationId !== undefined) body.extRelationId = extRelationId;
if (relStr !== undefined) body.relStr = relStr;
if (selfService !== undefined) body.selfService = selfService;
if (selfServiceHourlyCredit !== undefined) body.selfServiceHourlyCredit = selfServiceHourlyCredit;
if (selfServiceHourlyGroupProfiles !== undefined) body.selfServiceHourlyGroupProfiles = selfServiceHourlyGroupProfiles;
if (selfServiceResourceGroupProfiles !== undefined) body.selfServiceResourceGroupProfiles = selfServiceResourceGroupProfiles;
if (selfServiceHourlyResourcePack !== undefined) body.selfServiceHourlyResourcePack = selfServiceHourlyResourcePack;
if (sendMail !== undefined) body.sendMail = sendMail;
const result = await client.post('/users', body);
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'users_get',
'Retrieve a user by external relation ID',
{
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
},
async ({ extRelationId, relStr }) => {
try {
const result = await client.get(`/users/${extRelationId}/byExtRelation`, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'users_modify',
'Modify a user',
{
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
name: z.string().optional().describe('User display name'),
email: z.string().optional().describe('User email address'),
selfService: z.number().optional().describe('Self-service level (0-3)'),
selfServiceHourlyCredit: z.boolean().optional().describe('Enable hourly credit for self-service'),
selfServiceHourlyGroupProfiles: z.array(z.number()).optional().describe('Hourly group profile IDs'),
selfServiceResourceGroupProfiles: z.array(z.number()).optional().describe('Resource group profile IDs'),
selfServiceHourlyResourcePack: z.number().optional().describe('Hourly resource pack ID'),
enabled: z.boolean().optional().describe('Enable or disable the user'),
},
async ({ extRelationId, relStr, name, email, selfService, selfServiceHourlyCredit, selfServiceHourlyGroupProfiles, selfServiceResourceGroupProfiles, selfServiceHourlyResourcePack, enabled }) => {
try {
const body: Record<string, unknown> = {};
if (name !== undefined) body.name = name;
if (email !== undefined) body.email = email;
if (selfService !== undefined) body.selfService = selfService;
if (selfServiceHourlyCredit !== undefined) body.selfServiceHourlyCredit = selfServiceHourlyCredit;
if (selfServiceHourlyGroupProfiles !== undefined) body.selfServiceHourlyGroupProfiles = selfServiceHourlyGroupProfiles;
if (selfServiceResourceGroupProfiles !== undefined) body.selfServiceResourceGroupProfiles = selfServiceResourceGroupProfiles;
if (selfServiceHourlyResourcePack !== undefined) body.selfServiceHourlyResourcePack = selfServiceHourlyResourcePack;
if (enabled !== undefined) body.enabled = enabled;
const result = await client.put(`/users/${extRelationId}/byExtRelation`, Object.keys(body).length > 0 ? body : undefined, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'users_delete',
'Delete a user',
{
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
},
async ({ extRelationId, relStr }) => {
try {
const result = await client.delete(`/users/${extRelationId}/byExtRelation`, undefined, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'users_reset_password',
"Reset a user's password",
{
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
},
async ({ extRelationId, relStr }) => {
try {
const result = await client.post(`/users/${extRelationId}/byExtRelation/resetPassword`, undefined, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'users_generate_auth_tokens',
'Generate login tokens for a user',
{
extRelationId: z.string().describe('External relation ID'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
},
async ({ extRelationId, relStr }) => {
try {
const result = await client.post(`/users/${extRelationId}/authenticationTokens`, undefined, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
server.tool(
'users_generate_server_auth_tokens',
'Generate login tokens for a user scoped to a specific server',
{
extRelationId: z.string().describe('External relation ID'),
serverId: z.number().describe('The server ID to scope the token to'),
relStr: z.boolean().optional().describe('Treat extRelationId as a relation string'),
},
async ({ extRelationId, serverId, relStr }) => {
try {
const result = await client.post(`/users/${extRelationId}/serverAuthenticationTokens/${serverId}`, undefined, { relStr });
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
} catch (error) {
return formatErrorResponse(error);
}
}
);
}

17
src/types.ts Normal file
View File

@@ -0,0 +1,17 @@
export interface PaginatedResponse<T> {
current_page: number;
data: T[];
first_page_url: string;
from: number | null;
last_page: number;
last_page_url: string;
links: Array<{ url: string | null; label: string; active: boolean }>;
next_page_url: string | null;
path: string;
per_page: number;
prev_page_url: string | null;
to: number | null;
total: number;
}
export type QueryParams = Record<string, string | number | boolean | undefined>;

19
tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "scripts"]
}