feat: initial implementation of VirtFusion MCP server
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:
25
.gitea/workflows/ci.yaml
Normal file
25
.gitea/workflows/ci.yaml
Normal 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
|
||||
27
.gitea/workflows/endpoint-sync.yaml
Normal file
27
.gitea/workflows/endpoint-sync.yaml
Normal 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
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.js.map
|
||||
*.d.ts.map
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
81
CLAUDE.md
Normal file
81
CLAUDE.md
Normal 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
21
LICENSE
Normal 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
301
README.md
Normal 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
506
endpoint-manifest.json
Normal 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
8309
openapi.yaml
Normal file
File diff suppressed because one or more lines are too long
1744
package-lock.json
generated
Normal file
1744
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
package.json
Normal file
43
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
79
scripts/check-endpoint-drift.ts
Normal file
79
scripts/check-endpoint-drift.ts
Normal 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);
|
||||
32
scripts/extract-endpoints.ts
Normal file
32
scripts/extract-endpoints.ts
Normal 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
76
src/client.ts
Normal 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
41
src/errors.ts
Normal 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
40
src/index.ts
Normal 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
22
src/tools/backups.ts
Normal 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
22
src/tools/dns.ts
Normal 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
20
src/tools/general.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
54
src/tools/hypervisor-groups.ts
Normal file
54
src/tools/hypervisor-groups.ts
Normal 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
38
src/tools/hypervisors.ts
Normal 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
39
src/tools/index.ts
Normal 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
57
src/tools/ip-blocks.ts
Normal 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
38
src/tools/media.ts
Normal 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
36
src/tools/packages.ts
Normal 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
22
src/tools/queue.ts
Normal 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
348
src/tools/self-service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
79
src/tools/servers-firewall.ts
Normal file
79
src/tools/servers-firewall.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
105
src/tools/servers-network.ts
Normal file
105
src/tools/servers-network.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
70
src/tools/servers-power.ts
Normal file
70
src/tools/servers-power.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
74
src/tools/servers-traffic.ts
Normal file
74
src/tools/servers-traffic.ts
Normal 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
444
src/tools/servers.ts
Normal 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
72
src/tools/ssh-keys.ts
Normal 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
162
src/tools/users.ts
Normal 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
17
src/types.ts
Normal 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
19
tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user