@ebowwa/hetzner-mcp 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +250 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1928 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/hetzner/actions.d.ts +356 -0
- package/dist/lib/hetzner/actions.d.ts.map +1 -0
- package/dist/lib/hetzner/actions.js +804 -0
- package/dist/lib/hetzner/actions.js.map +1 -0
- package/dist/lib/hetzner/auth.d.ts +7 -0
- package/dist/lib/hetzner/auth.d.ts.map +1 -0
- package/dist/lib/hetzner/auth.js +35 -0
- package/dist/lib/hetzner/auth.js.map +1 -0
- package/dist/lib/hetzner/bootstrap/cloud-init.d.ts +79 -0
- package/dist/lib/hetzner/bootstrap/cloud-init.d.ts.map +1 -0
- package/dist/lib/hetzner/bootstrap/cloud-init.js +279 -0
- package/dist/lib/hetzner/bootstrap/cloud-init.js.map +1 -0
- package/dist/lib/hetzner/bootstrap/firewall.d.ts +119 -0
- package/dist/lib/hetzner/bootstrap/firewall.d.ts.map +1 -0
- package/dist/lib/hetzner/bootstrap/firewall.js +279 -0
- package/dist/lib/hetzner/bootstrap/firewall.js.map +1 -0
- package/dist/lib/hetzner/bootstrap/genesis.d.ts +83 -0
- package/dist/lib/hetzner/bootstrap/genesis.d.ts.map +1 -0
- package/dist/lib/hetzner/bootstrap/genesis.js +406 -0
- package/dist/lib/hetzner/bootstrap/genesis.js.map +1 -0
- package/dist/lib/hetzner/bootstrap/index.d.ts +30 -0
- package/dist/lib/hetzner/bootstrap/index.d.ts.map +1 -0
- package/dist/lib/hetzner/bootstrap/index.js +35 -0
- package/dist/lib/hetzner/bootstrap/index.js.map +1 -0
- package/dist/lib/hetzner/bootstrap/kernel-hardening.d.ts +70 -0
- package/dist/lib/hetzner/bootstrap/kernel-hardening.d.ts.map +1 -0
- package/dist/lib/hetzner/bootstrap/kernel-hardening.js +266 -0
- package/dist/lib/hetzner/bootstrap/kernel-hardening.js.map +1 -0
- package/dist/lib/hetzner/bootstrap/kernel-hardening.test.d.ts +7 -0
- package/dist/lib/hetzner/bootstrap/kernel-hardening.test.d.ts.map +1 -0
- package/dist/lib/hetzner/bootstrap/kernel-hardening.test.js +181 -0
- package/dist/lib/hetzner/bootstrap/kernel-hardening.test.js.map +1 -0
- package/dist/lib/hetzner/bootstrap/security-audit.d.ts +46 -0
- package/dist/lib/hetzner/bootstrap/security-audit.d.ts.map +1 -0
- package/dist/lib/hetzner/bootstrap/security-audit.js +118 -0
- package/dist/lib/hetzner/bootstrap/security-audit.js.map +1 -0
- package/dist/lib/hetzner/bootstrap/ssh-hardening.d.ts +68 -0
- package/dist/lib/hetzner/bootstrap/ssh-hardening.d.ts.map +1 -0
- package/dist/lib/hetzner/bootstrap/ssh-hardening.js +182 -0
- package/dist/lib/hetzner/bootstrap/ssh-hardening.js.map +1 -0
- package/dist/lib/hetzner/client.d.ts +63 -0
- package/dist/lib/hetzner/client.d.ts.map +1 -0
- package/dist/lib/hetzner/client.js +137 -0
- package/dist/lib/hetzner/client.js.map +1 -0
- package/dist/lib/hetzner/config.d.ts +5 -0
- package/dist/lib/hetzner/config.d.ts.map +1 -0
- package/dist/lib/hetzner/config.js +5 -0
- package/dist/lib/hetzner/config.js.map +1 -0
- package/dist/lib/hetzner/errors.d.ts +171 -0
- package/dist/lib/hetzner/errors.d.ts.map +1 -0
- package/dist/lib/hetzner/errors.js +270 -0
- package/dist/lib/hetzner/errors.js.map +1 -0
- package/dist/lib/hetzner/index.d.ts +21 -0
- package/dist/lib/hetzner/index.d.ts.map +1 -0
- package/dist/lib/hetzner/index.js +29 -0
- package/dist/lib/hetzner/index.js.map +1 -0
- package/dist/lib/hetzner/pricing.d.ts +207 -0
- package/dist/lib/hetzner/pricing.d.ts.map +1 -0
- package/dist/lib/hetzner/pricing.js +284 -0
- package/dist/lib/hetzner/pricing.js.map +1 -0
- package/dist/lib/hetzner/schemas.d.ts +1644 -0
- package/dist/lib/hetzner/schemas.d.ts.map +1 -0
- package/dist/lib/hetzner/schemas.js +660 -0
- package/dist/lib/hetzner/schemas.js.map +1 -0
- package/dist/lib/hetzner/server-status.d.ts +26 -0
- package/dist/lib/hetzner/server-status.d.ts.map +1 -0
- package/dist/lib/hetzner/server-status.js +64 -0
- package/dist/lib/hetzner/server-status.js.map +1 -0
- package/dist/lib/hetzner/servers.d.ts +165 -0
- package/dist/lib/hetzner/servers.d.ts.map +1 -0
- package/dist/lib/hetzner/servers.js +424 -0
- package/dist/lib/hetzner/servers.js.map +1 -0
- package/dist/lib/hetzner/ssh-keys.d.ts +36 -0
- package/dist/lib/hetzner/ssh-keys.d.ts.map +1 -0
- package/dist/lib/hetzner/ssh-keys.js +90 -0
- package/dist/lib/hetzner/ssh-keys.js.map +1 -0
- package/dist/lib/hetzner/ssh-setup.d.ts +48 -0
- package/dist/lib/hetzner/ssh-setup.d.ts.map +1 -0
- package/dist/lib/hetzner/ssh-setup.js +167 -0
- package/dist/lib/hetzner/ssh-setup.js.map +1 -0
- package/dist/lib/hetzner/types.d.ts +330 -0
- package/dist/lib/hetzner/types.d.ts.map +1 -0
- package/dist/lib/hetzner/types.js +96 -0
- package/dist/lib/hetzner/types.js.map +1 -0
- package/dist/lib/hetzner/volumes.d.ts +106 -0
- package/dist/lib/hetzner/volumes.d.ts.map +1 -0
- package/dist/lib/hetzner/volumes.js +172 -0
- package/dist/lib/hetzner/volumes.js.map +1 -0
- package/dist/lib/resources.d.ts +70 -0
- package/dist/lib/resources.d.ts.map +1 -0
- package/dist/lib/resources.js +115 -0
- package/dist/lib/resources.js.map +1 -0
- package/dist/lib/ssh/flags.d.ts +349 -0
- package/dist/lib/ssh/flags.d.ts.map +1 -0
- package/dist/lib/ssh/flags.js +322 -0
- package/dist/lib/ssh/flags.js.map +1 -0
- package/dist/lib/ssh/index.d.ts +5 -0
- package/dist/lib/ssh/index.d.ts.map +1 -0
- package/dist/lib/ssh/index.js +5 -0
- package/dist/lib/ssh/index.js.map +1 -0
- package/dist/lib/terminal/api.d.ts +8 -0
- package/dist/lib/terminal/api.d.ts.map +1 -0
- package/dist/lib/terminal/api.js +594 -0
- package/dist/lib/terminal/api.js.map +1 -0
- package/dist/lib/terminal/client.d.ts +15 -0
- package/dist/lib/terminal/client.d.ts.map +1 -0
- package/dist/lib/terminal/client.js +45 -0
- package/dist/lib/terminal/client.js.map +1 -0
- package/dist/lib/terminal/config.d.ts +86 -0
- package/dist/lib/terminal/config.d.ts.map +1 -0
- package/dist/lib/terminal/config.js +375 -0
- package/dist/lib/terminal/config.js.map +1 -0
- package/dist/lib/terminal/error.d.ts +8 -0
- package/dist/lib/terminal/error.d.ts.map +1 -0
- package/dist/lib/terminal/error.js +12 -0
- package/dist/lib/terminal/error.js.map +1 -0
- package/dist/lib/terminal/exec.d.ts +47 -0
- package/dist/lib/terminal/exec.d.ts.map +1 -0
- package/dist/lib/terminal/exec.js +107 -0
- package/dist/lib/terminal/exec.js.map +1 -0
- package/dist/lib/terminal/files.d.ts +124 -0
- package/dist/lib/terminal/files.d.ts.map +1 -0
- package/dist/lib/terminal/files.js +436 -0
- package/dist/lib/terminal/files.js.map +1 -0
- package/dist/lib/terminal/fingerprint.d.ts +67 -0
- package/dist/lib/terminal/fingerprint.d.ts.map +1 -0
- package/dist/lib/terminal/fingerprint.js +214 -0
- package/dist/lib/terminal/fingerprint.js.map +1 -0
- package/dist/lib/terminal/index.d.ts +14 -0
- package/dist/lib/terminal/index.d.ts.map +1 -0
- package/dist/lib/terminal/index.js +21 -0
- package/dist/lib/terminal/index.js.map +1 -0
- package/dist/lib/terminal/manager.d.ts +103 -0
- package/dist/lib/terminal/manager.d.ts.map +1 -0
- package/dist/lib/terminal/manager.js +237 -0
- package/dist/lib/terminal/manager.js.map +1 -0
- package/dist/lib/terminal/network-error-detector.d.ts +19 -0
- package/dist/lib/terminal/network-error-detector.d.ts.map +1 -0
- package/dist/lib/terminal/network-error-detector.js +98 -0
- package/dist/lib/terminal/network-error-detector.js.map +1 -0
- package/dist/lib/terminal/pool.d.ts +143 -0
- package/dist/lib/terminal/pool.d.ts.map +1 -0
- package/dist/lib/terminal/pool.js +554 -0
- package/dist/lib/terminal/pool.js.map +1 -0
- package/dist/lib/terminal/pty.d.ts +59 -0
- package/dist/lib/terminal/pty.d.ts.map +1 -0
- package/dist/lib/terminal/pty.js +224 -0
- package/dist/lib/terminal/pty.js.map +1 -0
- package/dist/lib/terminal/resources.d.ts +63 -0
- package/dist/lib/terminal/resources.d.ts.map +1 -0
- package/dist/lib/terminal/resources.js +62 -0
- package/dist/lib/terminal/resources.js.map +1 -0
- package/dist/lib/terminal/scp.d.ts +30 -0
- package/dist/lib/terminal/scp.d.ts.map +1 -0
- package/dist/lib/terminal/scp.js +74 -0
- package/dist/lib/terminal/scp.js.map +1 -0
- package/dist/lib/terminal/sessions.d.ts +101 -0
- package/dist/lib/terminal/sessions.d.ts.map +1 -0
- package/dist/lib/terminal/sessions.js +718 -0
- package/dist/lib/terminal/sessions.js.map +1 -0
- package/dist/lib/terminal/tmux-exec.d.ts +50 -0
- package/dist/lib/terminal/tmux-exec.d.ts.map +1 -0
- package/dist/lib/terminal/tmux-exec.js +79 -0
- package/dist/lib/terminal/tmux-exec.js.map +1 -0
- package/dist/lib/terminal/tmux-local.d.ts +273 -0
- package/dist/lib/terminal/tmux-local.d.ts.map +1 -0
- package/dist/lib/terminal/tmux-local.js +629 -0
- package/dist/lib/terminal/tmux-local.js.map +1 -0
- package/dist/lib/terminal/tmux-manager.d.ts +328 -0
- package/dist/lib/terminal/tmux-manager.d.ts.map +1 -0
- package/dist/lib/terminal/tmux-manager.js +667 -0
- package/dist/lib/terminal/tmux-manager.js.map +1 -0
- package/dist/lib/terminal/tmux.d.ts +213 -0
- package/dist/lib/terminal/tmux.d.ts.map +1 -0
- package/dist/lib/terminal/tmux.js +528 -0
- package/dist/lib/terminal/tmux.js.map +1 -0
- package/dist/lib/terminal/types.d.ts +18 -0
- package/dist/lib/terminal/types.d.ts.map +1 -0
- package/dist/lib/terminal/types.js +5 -0
- package/dist/lib/terminal/types.js.map +1 -0
- package/lmdb.db +0 -0
- package/lmdb.db-lock +0 -0
- package/package.json +48 -0
- package/src/index.js +2034 -0
- package/src/index.ts +2295 -0
- package/src/lib/hetzner/actions.ts +1056 -0
- package/src/lib/hetzner/auth.ts +37 -0
- package/src/lib/hetzner/bootstrap/cloud-init.ts +394 -0
- package/src/lib/hetzner/bootstrap/firewall.ts +342 -0
- package/src/lib/hetzner/bootstrap/genesis.ts +518 -0
- package/src/lib/hetzner/bootstrap/index.ts +71 -0
- package/src/lib/hetzner/bootstrap/kernel-hardening.test.ts +230 -0
- package/src/lib/hetzner/bootstrap/kernel-hardening.ts +272 -0
- package/src/lib/hetzner/bootstrap/security-audit.ts +124 -0
- package/src/lib/hetzner/bootstrap/ssh-hardening.ts +192 -0
- package/src/lib/hetzner/client.ts +177 -0
- package/src/lib/hetzner/config.ts +5 -0
- package/src/lib/hetzner/errors.ts +371 -0
- package/src/lib/hetzner/index.ts +56 -0
- package/src/lib/hetzner/pricing.ts +422 -0
- package/src/lib/hetzner/schemas.ts +765 -0
- package/src/lib/hetzner/server-status.ts +81 -0
- package/src/lib/hetzner/servers.ts +568 -0
- package/src/lib/hetzner/ssh-keys.ts +122 -0
- package/src/lib/hetzner/ssh-setup.ts +218 -0
- package/src/lib/hetzner/types.ts +419 -0
- package/src/lib/hetzner/volumes.ts +229 -0
- package/src/lib/resources.ts +156 -0
- package/src/lib/ssh/flags.ts +578 -0
- package/src/lib/ssh/index.ts +5 -0
- package/src/lib/terminal/client.ts +55 -0
- package/src/lib/terminal/config.ts +489 -0
- package/src/lib/terminal/error.ts +13 -0
- package/src/lib/terminal/exec.ts +128 -0
- package/src/lib/terminal/files.ts +636 -0
- package/src/lib/terminal/index.ts +71 -0
- package/src/lib/terminal/pool.ts +662 -0
- package/src/lib/terminal/scp.ts +109 -0
- package/src/lib/terminal/tmux-exec.ts +96 -0
- package/src/lib/terminal/tmux.ts +711 -0
- package/src/lib/terminal/types.ts +19 -0
- package/tsconfig.json +20 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1928 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @mcp/hetzner - Hetzner VPS Management MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Full-featured Hetzner Cloud integration with:
|
|
6
|
+
* - Server management (list, create, delete, power operations)
|
|
7
|
+
* - SSH/tmux session management via @ebowwa/terminal
|
|
8
|
+
* - Advanced tmux operations (windows, panes, split, switch)
|
|
9
|
+
* - File operations (SCP upload/download)
|
|
10
|
+
* - Volume operations (create, delete, attach, detach, resize)
|
|
11
|
+
* - Resource monitoring (CPU, memory, disk, GPU, network)
|
|
12
|
+
* - SSH key management
|
|
13
|
+
* - Action monitoring
|
|
14
|
+
*/
|
|
15
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
18
|
+
import { HetznerClient, generateSeedBootstrap, VolumeOperations } from "./lib/hetzner/index.js";
|
|
19
|
+
import { execSSH } from "./lib/terminal/client.js";
|
|
20
|
+
import { testSSHConnection, } from "./lib/terminal/exec.js";
|
|
21
|
+
import { parseResources } from "./lib/resources.js";
|
|
22
|
+
import { createOrAttachTmuxSession, killTmuxSession, listTmuxSessions, sendCommandToPane, capturePane, splitPane, cleanupOldTmuxSessions, listSessionWindows, listWindowPanes, switchWindow, switchPane, renameWindow, killPane, } from "./lib/terminal/tmux.js";
|
|
23
|
+
import { scpUpload, scpDownload, } from "./lib/terminal/scp.js";
|
|
24
|
+
import { listFiles, previewFile, } from "./lib/terminal/files.js";
|
|
25
|
+
// ==============
|
|
26
|
+
// Resource Commands
|
|
27
|
+
//=============
|
|
28
|
+
const RESOURCE_COMMANDS = {
|
|
29
|
+
cpu: "top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1",
|
|
30
|
+
memory: "free | awk '/^Mem:/ {printf \"%.1f %.1f %.1f\", $3/$2*100, $3/1024, $2/1024}'",
|
|
31
|
+
disk: "df -h / | awk 'NR==2 {printf \"%s %s %s\", $5, $3, $2}'",
|
|
32
|
+
gpu: "nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv,noheader,nounits 2>/dev/null || echo 'NOGPU'",
|
|
33
|
+
loadavg: "cat /proc/loadavg | awk '{print $1}'",
|
|
34
|
+
processes: "ps aux | wc -l",
|
|
35
|
+
connections: "netstat -an 2>/dev/null | grep ESTABLISHED | wc -l || ss -an 2>/dev/null | grep ESTAB | wc -l",
|
|
36
|
+
ports: "netstat -tuln 2>/dev/null | grep LISTEN | wc -l || ss -tuln 2>/dev/null | grep LISTEN | wc -l",
|
|
37
|
+
};
|
|
38
|
+
import { homedir } from "os";
|
|
39
|
+
import { join } from "path";
|
|
40
|
+
async function getTokenFromCLI() {
|
|
41
|
+
try {
|
|
42
|
+
const configPath = join(homedir(), ".config", "hcloud", "cli.toml");
|
|
43
|
+
// @ts-ignore - Bun.file is available
|
|
44
|
+
const configFile = Bun.file(configPath);
|
|
45
|
+
if (await configFile.exists()) {
|
|
46
|
+
const config = await configFile.text();
|
|
47
|
+
const match = config.match(/token\s*=\s*["']([^"']+)["']/);
|
|
48
|
+
if (match && match[1]) {
|
|
49
|
+
return match[1];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
// Ignore errors
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
// Initialize client
|
|
59
|
+
let hetznerClient = null;
|
|
60
|
+
async function getHetznerClient() {
|
|
61
|
+
if (!hetznerClient) {
|
|
62
|
+
const token = process.env.HETZNER_API_TOKEN || await getTokenFromCLI();
|
|
63
|
+
if (!token) {
|
|
64
|
+
throw new Error("HETZNER_API_TOKEN not configured. Set environment variable or run `hcloud context create`.");
|
|
65
|
+
}
|
|
66
|
+
hetznerClient = new HetznerClient(token);
|
|
67
|
+
}
|
|
68
|
+
return hetznerClient;
|
|
69
|
+
}
|
|
70
|
+
// ==============
|
|
71
|
+
// MCP Server
|
|
72
|
+
//=============
|
|
73
|
+
const server = new Server({
|
|
74
|
+
name: "@mcp/hetzner",
|
|
75
|
+
version: "1.1.0",
|
|
76
|
+
}, {
|
|
77
|
+
capabilities: {
|
|
78
|
+
tools: {},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
// List available tools
|
|
82
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
83
|
+
return {
|
|
84
|
+
tools: [
|
|
85
|
+
// Server Management
|
|
86
|
+
{
|
|
87
|
+
name: "list_servers",
|
|
88
|
+
description: "List all Hetzner servers with their status, IPs, and details",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
status: {
|
|
93
|
+
type: "string",
|
|
94
|
+
description: "Filter by server status (running, stopped, initializing, etc.)",
|
|
95
|
+
enum: ["running", "stopped", "initializing", "starting", "stopping"],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "get_server",
|
|
102
|
+
description: "Get detailed information about a specific server",
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
id: {
|
|
107
|
+
type: "string",
|
|
108
|
+
description: "Server ID or name",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
required: ["id"],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "get_server_types",
|
|
116
|
+
description: "List available Hetzner server types with pricing",
|
|
117
|
+
inputSchema: {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "get_datacenters",
|
|
124
|
+
description: "List Hetzner datacenters and locations",
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "get_locations",
|
|
132
|
+
description: "List available Hetzner locations",
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "get_ssh_keys",
|
|
140
|
+
description: "List SSH keys registered in Hetzner",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "create_server",
|
|
148
|
+
description: "Create a new Hetzner server",
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: {
|
|
152
|
+
name: {
|
|
153
|
+
type: "string",
|
|
154
|
+
description: "Server name",
|
|
155
|
+
},
|
|
156
|
+
server_type: {
|
|
157
|
+
type: "string",
|
|
158
|
+
description: "Server type (e.g., cx22.medium)",
|
|
159
|
+
},
|
|
160
|
+
image: {
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "Image ID or name",
|
|
163
|
+
},
|
|
164
|
+
location: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: "Location (e.g., nbg1, fsn1)",
|
|
167
|
+
},
|
|
168
|
+
ssh_keys: {
|
|
169
|
+
type: "array",
|
|
170
|
+
items: { type: "string" },
|
|
171
|
+
description: "SSH key IDs or fingerprints",
|
|
172
|
+
},
|
|
173
|
+
user_data: {
|
|
174
|
+
type: "string",
|
|
175
|
+
description: "Cloud-init user data (must start with #cloud-config)",
|
|
176
|
+
},
|
|
177
|
+
start_after_create: {
|
|
178
|
+
type: "boolean",
|
|
179
|
+
description: "Start server after creation (default: true)",
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
required: ["name", "server_type", "image", "location"],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "create_server_hardened",
|
|
187
|
+
description: "Create a new Hetzner server with security hardening (SSH keys only, fail2ban, UFW firewall, kernel hardening). Uses cloud-init to apply security at first boot.",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: "object",
|
|
190
|
+
properties: {
|
|
191
|
+
name: {
|
|
192
|
+
type: "string",
|
|
193
|
+
description: "Server name",
|
|
194
|
+
},
|
|
195
|
+
server_type: {
|
|
196
|
+
type: "string",
|
|
197
|
+
description: "Server type (e.g., cx22.medium)",
|
|
198
|
+
},
|
|
199
|
+
image: {
|
|
200
|
+
type: "string",
|
|
201
|
+
description: "Image ID or name (Ubuntu 24.04 recommended)",
|
|
202
|
+
},
|
|
203
|
+
location: {
|
|
204
|
+
type: "string",
|
|
205
|
+
description: "Location (e.g., nbg1, fsn1)",
|
|
206
|
+
},
|
|
207
|
+
ssh_keys: {
|
|
208
|
+
type: "array",
|
|
209
|
+
items: { type: "string" },
|
|
210
|
+
description: "SSH key IDs or fingerprints (required - password auth disabled)",
|
|
211
|
+
},
|
|
212
|
+
enable_security: {
|
|
213
|
+
type: "boolean",
|
|
214
|
+
description: "Enable security hardening (default: true)",
|
|
215
|
+
},
|
|
216
|
+
allow_http: {
|
|
217
|
+
type: "boolean",
|
|
218
|
+
description: "Allow HTTP port 80 (default: false)",
|
|
219
|
+
},
|
|
220
|
+
allow_https: {
|
|
221
|
+
type: "boolean",
|
|
222
|
+
description: "Allow HTTPS port 443 (default: false)",
|
|
223
|
+
},
|
|
224
|
+
start_after_create: {
|
|
225
|
+
type: "boolean",
|
|
226
|
+
description: "Start server after creation (default: true)",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
required: ["name", "server_type", "image", "location", "ssh_keys"],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: "delete_server",
|
|
234
|
+
description: "Delete a Hetzner server",
|
|
235
|
+
inputSchema: {
|
|
236
|
+
type: "object",
|
|
237
|
+
properties: {
|
|
238
|
+
id: {
|
|
239
|
+
type: "string",
|
|
240
|
+
description: "Server ID",
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
required: ["id"],
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: "power_on",
|
|
248
|
+
description: "Power on a stopped server",
|
|
249
|
+
inputSchema: {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: {
|
|
252
|
+
id: {
|
|
253
|
+
type: "string",
|
|
254
|
+
description: "Server ID",
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
required: ["id"],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: "power_off",
|
|
262
|
+
description: "Power off a running server",
|
|
263
|
+
inputSchema: {
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: {
|
|
266
|
+
id: {
|
|
267
|
+
type: "string",
|
|
268
|
+
description: "Server ID",
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
required: ["id"],
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: "reboot",
|
|
276
|
+
description: "Reboot a server",
|
|
277
|
+
inputSchema: {
|
|
278
|
+
type: "object",
|
|
279
|
+
properties: {
|
|
280
|
+
id: {
|
|
281
|
+
type: "string",
|
|
282
|
+
description: "Server ID",
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
required: ["id"],
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "get_actions",
|
|
290
|
+
description: "Get recent actions for a server",
|
|
291
|
+
inputSchema: {
|
|
292
|
+
type: "object",
|
|
293
|
+
properties: {
|
|
294
|
+
id: {
|
|
295
|
+
type: "string",
|
|
296
|
+
description: "Server ID",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
required: ["id"],
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
// SSH Operations
|
|
303
|
+
{
|
|
304
|
+
name: "ssh_exec",
|
|
305
|
+
description: "Execute a command on a server via SSH",
|
|
306
|
+
inputSchema: {
|
|
307
|
+
type: "object",
|
|
308
|
+
properties: {
|
|
309
|
+
host: {
|
|
310
|
+
type: "string",
|
|
311
|
+
description: "Server IP or hostname",
|
|
312
|
+
},
|
|
313
|
+
command: {
|
|
314
|
+
type: "string",
|
|
315
|
+
description: "Command to execute",
|
|
316
|
+
},
|
|
317
|
+
user: {
|
|
318
|
+
type: "string",
|
|
319
|
+
description: "SSH user (default: root)",
|
|
320
|
+
},
|
|
321
|
+
port: {
|
|
322
|
+
type: "number",
|
|
323
|
+
description: "SSH port (default: 22)",
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
required: ["host", "command"],
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
name: "ssh_test",
|
|
331
|
+
description: "Test SSH connection to a server",
|
|
332
|
+
inputSchema: {
|
|
333
|
+
type: "object",
|
|
334
|
+
properties: {
|
|
335
|
+
host: {
|
|
336
|
+
type: "string",
|
|
337
|
+
description: "Server IP or hostname",
|
|
338
|
+
},
|
|
339
|
+
user: {
|
|
340
|
+
type: "string",
|
|
341
|
+
description: "SSH user (default: root)",
|
|
342
|
+
},
|
|
343
|
+
port: {
|
|
344
|
+
type: "number",
|
|
345
|
+
description: "SSH port (default: 22)",
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
required: ["host"],
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: "ssh_exec_parallel",
|
|
353
|
+
description: "Execute a command on multiple servers in parallel",
|
|
354
|
+
inputSchema: {
|
|
355
|
+
type: "object",
|
|
356
|
+
properties: {
|
|
357
|
+
hosts: {
|
|
358
|
+
type: "array",
|
|
359
|
+
items: { type: "string" },
|
|
360
|
+
description: "List of server IPs/hostnames",
|
|
361
|
+
},
|
|
362
|
+
command: {
|
|
363
|
+
type: "string",
|
|
364
|
+
description: "Command to execute",
|
|
365
|
+
},
|
|
366
|
+
user: {
|
|
367
|
+
type: "string",
|
|
368
|
+
description: "SSH user (default: root)",
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
required: ["hosts", "command"],
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
// Tmux Operations
|
|
375
|
+
{
|
|
376
|
+
name: "tmux_create_or_attach",
|
|
377
|
+
description: "Create or attach to a tmux session on a server",
|
|
378
|
+
inputSchema: {
|
|
379
|
+
type: "object",
|
|
380
|
+
properties: {
|
|
381
|
+
host: {
|
|
382
|
+
type: "string",
|
|
383
|
+
description: "Server IP or hostname",
|
|
384
|
+
},
|
|
385
|
+
sessionName: {
|
|
386
|
+
type: "string",
|
|
387
|
+
description: "Session name (auto-generated if not provided)",
|
|
388
|
+
},
|
|
389
|
+
user: {
|
|
390
|
+
type: "string",
|
|
391
|
+
description: "SSH user (default: root)",
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
required: ["host"],
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "tmux_list",
|
|
399
|
+
description: "List tmux sessions on a server",
|
|
400
|
+
inputSchema: {
|
|
401
|
+
type: "object",
|
|
402
|
+
properties: {
|
|
403
|
+
host: {
|
|
404
|
+
type: "string",
|
|
405
|
+
description: "Server IP or hostname",
|
|
406
|
+
},
|
|
407
|
+
user: {
|
|
408
|
+
type: "string",
|
|
409
|
+
description: "SSH user (default: root)",
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
required: ["host"],
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
name: "tmux_kill",
|
|
417
|
+
description: "Kill a tmux session on a server",
|
|
418
|
+
inputSchema: {
|
|
419
|
+
type: "object",
|
|
420
|
+
properties: {
|
|
421
|
+
host: {
|
|
422
|
+
type: "string",
|
|
423
|
+
description: "Server IP or hostname",
|
|
424
|
+
},
|
|
425
|
+
sessionName: {
|
|
426
|
+
type: "string",
|
|
427
|
+
description: "Session name",
|
|
428
|
+
},
|
|
429
|
+
user: {
|
|
430
|
+
type: "string",
|
|
431
|
+
description: "SSH user (default: root)",
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
required: ["host", "sessionName"],
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: "tmux_send_command",
|
|
439
|
+
description: "Send a command to a tmux session pane",
|
|
440
|
+
inputSchema: {
|
|
441
|
+
type: "object",
|
|
442
|
+
properties: {
|
|
443
|
+
host: {
|
|
444
|
+
type: "string",
|
|
445
|
+
description: "Server IP or hostname",
|
|
446
|
+
},
|
|
447
|
+
sessionName: {
|
|
448
|
+
type: "string",
|
|
449
|
+
description: "Session name",
|
|
450
|
+
},
|
|
451
|
+
command: {
|
|
452
|
+
type: "string",
|
|
453
|
+
description: "Command to send",
|
|
454
|
+
},
|
|
455
|
+
paneIndex: {
|
|
456
|
+
type: "string",
|
|
457
|
+
description: "Pane index (default: 0)",
|
|
458
|
+
},
|
|
459
|
+
user: {
|
|
460
|
+
type: "string",
|
|
461
|
+
description: "SSH user (default: root)",
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
required: ["host", "sessionName", "command"],
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
name: "tmux_capture",
|
|
469
|
+
description: "Capture output from a tmux pane",
|
|
470
|
+
inputSchema: {
|
|
471
|
+
type: "object",
|
|
472
|
+
properties: {
|
|
473
|
+
host: {
|
|
474
|
+
type: "string",
|
|
475
|
+
description: "Server IP or hostname",
|
|
476
|
+
},
|
|
477
|
+
sessionName: {
|
|
478
|
+
type: "string",
|
|
479
|
+
description: "Session name",
|
|
480
|
+
},
|
|
481
|
+
paneIndex: {
|
|
482
|
+
type: "string",
|
|
483
|
+
description: "Pane index (default: 0)",
|
|
484
|
+
},
|
|
485
|
+
user: {
|
|
486
|
+
type: "string",
|
|
487
|
+
description: "SSH user (default: root)",
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
required: ["host", "sessionName"],
|
|
491
|
+
},
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
name: "tmux_cleanup",
|
|
495
|
+
description: "Clean up old tmux sessions on a server",
|
|
496
|
+
inputSchema: {
|
|
497
|
+
type: "object",
|
|
498
|
+
properties: {
|
|
499
|
+
host: {
|
|
500
|
+
type: "string",
|
|
501
|
+
description: "Server IP or hostname",
|
|
502
|
+
},
|
|
503
|
+
ageLimit: {
|
|
504
|
+
type: "number",
|
|
505
|
+
description: "Age limit in days (default: 30)",
|
|
506
|
+
},
|
|
507
|
+
user: {
|
|
508
|
+
type: "string",
|
|
509
|
+
description: "SSH user (default: root)",
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
required: ["host"],
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: "tmux_list_windows",
|
|
517
|
+
description: "List all windows in a tmux session",
|
|
518
|
+
inputSchema: {
|
|
519
|
+
type: "object",
|
|
520
|
+
properties: {
|
|
521
|
+
host: {
|
|
522
|
+
type: "string",
|
|
523
|
+
description: "Server IP or hostname",
|
|
524
|
+
},
|
|
525
|
+
sessionName: {
|
|
526
|
+
type: "string",
|
|
527
|
+
description: "Session name",
|
|
528
|
+
},
|
|
529
|
+
user: {
|
|
530
|
+
type: "string",
|
|
531
|
+
description: "SSH user (default: root)",
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
required: ["host", "sessionName"],
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
name: "tmux_list_panes",
|
|
539
|
+
description: "List all panes in a tmux window",
|
|
540
|
+
inputSchema: {
|
|
541
|
+
type: "object",
|
|
542
|
+
properties: {
|
|
543
|
+
host: {
|
|
544
|
+
type: "string",
|
|
545
|
+
description: "Server IP or hostname",
|
|
546
|
+
},
|
|
547
|
+
sessionName: {
|
|
548
|
+
type: "string",
|
|
549
|
+
description: "Session name",
|
|
550
|
+
},
|
|
551
|
+
windowIndex: {
|
|
552
|
+
type: "string",
|
|
553
|
+
description: "Window index (default: 0)",
|
|
554
|
+
},
|
|
555
|
+
user: {
|
|
556
|
+
type: "string",
|
|
557
|
+
description: "SSH user (default: root)",
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
required: ["host", "sessionName"],
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: "tmux_split_pane",
|
|
565
|
+
description: "Split a tmux pane (creates a new pane in current window)",
|
|
566
|
+
inputSchema: {
|
|
567
|
+
type: "object",
|
|
568
|
+
properties: {
|
|
569
|
+
host: {
|
|
570
|
+
type: "string",
|
|
571
|
+
description: "Server IP or hostname",
|
|
572
|
+
},
|
|
573
|
+
sessionName: {
|
|
574
|
+
type: "string",
|
|
575
|
+
description: "Session name",
|
|
576
|
+
},
|
|
577
|
+
windowIndex: {
|
|
578
|
+
type: "string",
|
|
579
|
+
description: "Window index (default: 0)",
|
|
580
|
+
},
|
|
581
|
+
paneIndex: {
|
|
582
|
+
type: "string",
|
|
583
|
+
description: "Pane index to split (default: 0)",
|
|
584
|
+
},
|
|
585
|
+
direction: {
|
|
586
|
+
type: "string",
|
|
587
|
+
description: "Split direction: 'h' for horizontal, 'v' for vertical (default: 'h')",
|
|
588
|
+
enum: ["h", "v"],
|
|
589
|
+
},
|
|
590
|
+
user: {
|
|
591
|
+
type: "string",
|
|
592
|
+
description: "SSH user (default: root)",
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
required: ["host", "sessionName"],
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
{
|
|
599
|
+
name: "tmux_kill_pane",
|
|
600
|
+
description: "Kill a specific tmux pane",
|
|
601
|
+
inputSchema: {
|
|
602
|
+
type: "object",
|
|
603
|
+
properties: {
|
|
604
|
+
host: {
|
|
605
|
+
type: "string",
|
|
606
|
+
description: "Server IP or hostname",
|
|
607
|
+
},
|
|
608
|
+
sessionName: {
|
|
609
|
+
type: "string",
|
|
610
|
+
description: "Session name",
|
|
611
|
+
},
|
|
612
|
+
windowIndex: {
|
|
613
|
+
type: "string",
|
|
614
|
+
description: "Window index (default: 0)",
|
|
615
|
+
},
|
|
616
|
+
paneIndex: {
|
|
617
|
+
type: "string",
|
|
618
|
+
description: "Pane index to kill (required)",
|
|
619
|
+
},
|
|
620
|
+
user: {
|
|
621
|
+
type: "string",
|
|
622
|
+
description: "SSH user (default: root)",
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
required: ["host", "sessionName", "paneIndex"],
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
name: "tmux_switch_window",
|
|
630
|
+
description: "Switch to a different window in a tmux session",
|
|
631
|
+
inputSchema: {
|
|
632
|
+
type: "object",
|
|
633
|
+
properties: {
|
|
634
|
+
host: {
|
|
635
|
+
type: "string",
|
|
636
|
+
description: "Server IP or hostname",
|
|
637
|
+
},
|
|
638
|
+
sessionName: {
|
|
639
|
+
type: "string",
|
|
640
|
+
description: "Session name",
|
|
641
|
+
},
|
|
642
|
+
windowIndex: {
|
|
643
|
+
type: "string",
|
|
644
|
+
description: "Window index to switch to",
|
|
645
|
+
},
|
|
646
|
+
user: {
|
|
647
|
+
type: "string",
|
|
648
|
+
description: "SSH user (default: root)",
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
required: ["host", "sessionName", "windowIndex"],
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
name: "tmux_switch_pane",
|
|
656
|
+
description: "Switch focus to a different pane in the current window",
|
|
657
|
+
inputSchema: {
|
|
658
|
+
type: "object",
|
|
659
|
+
properties: {
|
|
660
|
+
host: {
|
|
661
|
+
type: "string",
|
|
662
|
+
description: "Server IP or hostname",
|
|
663
|
+
},
|
|
664
|
+
sessionName: {
|
|
665
|
+
type: "string",
|
|
666
|
+
description: "Session name",
|
|
667
|
+
},
|
|
668
|
+
windowIndex: {
|
|
669
|
+
type: "string",
|
|
670
|
+
description: "Window index (default: 0)",
|
|
671
|
+
},
|
|
672
|
+
paneIndex: {
|
|
673
|
+
type: "string",
|
|
674
|
+
description: "Pane index to switch to",
|
|
675
|
+
},
|
|
676
|
+
user: {
|
|
677
|
+
type: "string",
|
|
678
|
+
description: "SSH user (default: root)",
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
required: ["host", "sessionName", "paneIndex"],
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
name: "tmux_rename_window",
|
|
686
|
+
description: "Rename a tmux window",
|
|
687
|
+
inputSchema: {
|
|
688
|
+
type: "object",
|
|
689
|
+
properties: {
|
|
690
|
+
host: {
|
|
691
|
+
type: "string",
|
|
692
|
+
description: "Server IP or hostname",
|
|
693
|
+
},
|
|
694
|
+
sessionName: {
|
|
695
|
+
type: "string",
|
|
696
|
+
description: "Session name",
|
|
697
|
+
},
|
|
698
|
+
windowIndex: {
|
|
699
|
+
type: "string",
|
|
700
|
+
description: "Window index (default: 0)",
|
|
701
|
+
},
|
|
702
|
+
newName: {
|
|
703
|
+
type: "string",
|
|
704
|
+
description: "New window name",
|
|
705
|
+
},
|
|
706
|
+
user: {
|
|
707
|
+
type: "string",
|
|
708
|
+
description: "SSH user (default: root)",
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
required: ["host", "sessionName", "newName"],
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
// File Operations
|
|
715
|
+
{
|
|
716
|
+
name: "scp_upload",
|
|
717
|
+
description: "Upload a file to a server via SCP",
|
|
718
|
+
inputSchema: {
|
|
719
|
+
type: "object",
|
|
720
|
+
properties: {
|
|
721
|
+
host: {
|
|
722
|
+
type: "string",
|
|
723
|
+
description: "Server IP or hostname",
|
|
724
|
+
},
|
|
725
|
+
source: {
|
|
726
|
+
type: "string",
|
|
727
|
+
description: "Local file path",
|
|
728
|
+
},
|
|
729
|
+
destination: {
|
|
730
|
+
type: "string",
|
|
731
|
+
description: "Remote file path",
|
|
732
|
+
},
|
|
733
|
+
user: {
|
|
734
|
+
type: "string",
|
|
735
|
+
description: "SSH user (default: root)",
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
required: ["host", "source", "destination"],
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
{
|
|
742
|
+
name: "scp_download",
|
|
743
|
+
description: "Download a file from a server via SCP",
|
|
744
|
+
inputSchema: {
|
|
745
|
+
type: "object",
|
|
746
|
+
properties: {
|
|
747
|
+
host: {
|
|
748
|
+
type: "string",
|
|
749
|
+
description: "Server IP or hostname",
|
|
750
|
+
},
|
|
751
|
+
source: {
|
|
752
|
+
type: "string",
|
|
753
|
+
description: "Remote file path",
|
|
754
|
+
},
|
|
755
|
+
destination: {
|
|
756
|
+
type: "string",
|
|
757
|
+
description: "Local file path",
|
|
758
|
+
},
|
|
759
|
+
user: {
|
|
760
|
+
type: "string",
|
|
761
|
+
description: "SSH user (default: root)",
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
required: ["host", "source", "destination"],
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
name: "file_list",
|
|
769
|
+
description: "List files in a directory on a server",
|
|
770
|
+
inputSchema: {
|
|
771
|
+
type: "object",
|
|
772
|
+
properties: {
|
|
773
|
+
host: {
|
|
774
|
+
type: "string",
|
|
775
|
+
description: "Server IP or hostname",
|
|
776
|
+
},
|
|
777
|
+
path: {
|
|
778
|
+
type: "string",
|
|
779
|
+
description: "Directory path (default: current directory)",
|
|
780
|
+
},
|
|
781
|
+
user: {
|
|
782
|
+
type: "string",
|
|
783
|
+
description: "SSH user (default: root)",
|
|
784
|
+
},
|
|
785
|
+
},
|
|
786
|
+
required: ["host"],
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
name: "file_preview",
|
|
791
|
+
description: "Preview a file on a server",
|
|
792
|
+
inputSchema: {
|
|
793
|
+
type: "object",
|
|
794
|
+
properties: {
|
|
795
|
+
host: {
|
|
796
|
+
type: "string",
|
|
797
|
+
description: "Server IP or hostname",
|
|
798
|
+
},
|
|
799
|
+
path: {
|
|
800
|
+
type: "string",
|
|
801
|
+
description: "File path",
|
|
802
|
+
},
|
|
803
|
+
lines: {
|
|
804
|
+
type: "number",
|
|
805
|
+
description: "Number of lines to preview (default: 100)",
|
|
806
|
+
},
|
|
807
|
+
user: {
|
|
808
|
+
type: "string",
|
|
809
|
+
description: "SSH user (default: root)",
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
required: ["host", "path"],
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
// Security Verification Tools
|
|
816
|
+
{
|
|
817
|
+
name: "check_bootstrap_status",
|
|
818
|
+
description: "Check if bootstrap/security hardening completed on a server",
|
|
819
|
+
inputSchema: {
|
|
820
|
+
type: "object",
|
|
821
|
+
properties: {
|
|
822
|
+
host: {
|
|
823
|
+
type: "string",
|
|
824
|
+
description: "Server IP or hostname",
|
|
825
|
+
},
|
|
826
|
+
user: {
|
|
827
|
+
type: "string",
|
|
828
|
+
description: "SSH user (default: root)",
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
required: ["host"],
|
|
832
|
+
},
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
name: "check_security_status",
|
|
836
|
+
description: "Check comprehensive security status (firewall, fail2ban, SSH, kernel)",
|
|
837
|
+
inputSchema: {
|
|
838
|
+
type: "object",
|
|
839
|
+
properties: {
|
|
840
|
+
host: {
|
|
841
|
+
type: "string",
|
|
842
|
+
description: "Server IP or hostname",
|
|
843
|
+
},
|
|
844
|
+
user: {
|
|
845
|
+
type: "string",
|
|
846
|
+
description: "SSH user (default: root)",
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
required: ["host"],
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
name: "get_ssh_logs",
|
|
854
|
+
description: "Get recent SSH connection logs and authentication events",
|
|
855
|
+
inputSchema: {
|
|
856
|
+
type: "object",
|
|
857
|
+
properties: {
|
|
858
|
+
host: {
|
|
859
|
+
type: "string",
|
|
860
|
+
description: "Server IP or hostname",
|
|
861
|
+
},
|
|
862
|
+
lines: {
|
|
863
|
+
type: "number",
|
|
864
|
+
description: "Number of log lines to show (default: 20)",
|
|
865
|
+
},
|
|
866
|
+
user: {
|
|
867
|
+
type: "string",
|
|
868
|
+
description: "SSH user (default: root)",
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
required: ["host"],
|
|
872
|
+
},
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
name: "check_fail2ban",
|
|
876
|
+
description: "Check fail2ban status, banned IPs, and failed login attempts",
|
|
877
|
+
inputSchema: {
|
|
878
|
+
type: "object",
|
|
879
|
+
properties: {
|
|
880
|
+
host: {
|
|
881
|
+
type: "string",
|
|
882
|
+
description: "Server IP or hostname",
|
|
883
|
+
},
|
|
884
|
+
user: {
|
|
885
|
+
type: "string",
|
|
886
|
+
description: "SSH user (default: root)",
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
required: ["host"],
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
name: "check_server_health",
|
|
894
|
+
description: "Get real-time server health metrics (CPU, memory, connections)",
|
|
895
|
+
inputSchema: {
|
|
896
|
+
type: "object",
|
|
897
|
+
properties: {
|
|
898
|
+
host: {
|
|
899
|
+
type: "string",
|
|
900
|
+
description: "Server IP or hostname",
|
|
901
|
+
},
|
|
902
|
+
user: {
|
|
903
|
+
type: "string",
|
|
904
|
+
description: "SSH user (default: root)",
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
required: ["host"],
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
// Volume Management
|
|
911
|
+
{
|
|
912
|
+
name: "list_volumes",
|
|
913
|
+
description: "List all Hetzner volumes with their status, size, and server attachments",
|
|
914
|
+
inputSchema: {
|
|
915
|
+
type: "object",
|
|
916
|
+
properties: {
|
|
917
|
+
name: {
|
|
918
|
+
type: "string",
|
|
919
|
+
description: "Filter by volume name",
|
|
920
|
+
},
|
|
921
|
+
status: {
|
|
922
|
+
type: "string",
|
|
923
|
+
description: "Filter by volume status",
|
|
924
|
+
},
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
name: "get_volume",
|
|
930
|
+
description: "Get detailed information about a specific volume",
|
|
931
|
+
inputSchema: {
|
|
932
|
+
type: "object",
|
|
933
|
+
properties: {
|
|
934
|
+
id: {
|
|
935
|
+
type: "string",
|
|
936
|
+
description: "Volume ID",
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
required: ["id"],
|
|
940
|
+
},
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
name: "create_volume",
|
|
944
|
+
description: "Create a new Hetzner volume",
|
|
945
|
+
inputSchema: {
|
|
946
|
+
type: "object",
|
|
947
|
+
properties: {
|
|
948
|
+
name: {
|
|
949
|
+
type: "string",
|
|
950
|
+
description: "Volume name",
|
|
951
|
+
},
|
|
952
|
+
size: {
|
|
953
|
+
type: "number",
|
|
954
|
+
description: "Volume size in GB",
|
|
955
|
+
},
|
|
956
|
+
server: {
|
|
957
|
+
type: "string",
|
|
958
|
+
description: "Server ID to attach volume to (optional)",
|
|
959
|
+
},
|
|
960
|
+
location: {
|
|
961
|
+
type: "string",
|
|
962
|
+
description: "Location (e.g., nbg1, fsn1)",
|
|
963
|
+
},
|
|
964
|
+
automount: {
|
|
965
|
+
type: "boolean",
|
|
966
|
+
description: "Automatically mount the volume (default: true)",
|
|
967
|
+
},
|
|
968
|
+
format: {
|
|
969
|
+
type: "string",
|
|
970
|
+
description: "File system format (e.g., ext4)",
|
|
971
|
+
},
|
|
972
|
+
},
|
|
973
|
+
required: ["name", "size"],
|
|
974
|
+
},
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
name: "delete_volume",
|
|
978
|
+
description: "Delete a volume permanently",
|
|
979
|
+
inputSchema: {
|
|
980
|
+
type: "object",
|
|
981
|
+
properties: {
|
|
982
|
+
id: {
|
|
983
|
+
type: "string",
|
|
984
|
+
description: "Volume ID",
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
required: ["id"],
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
{
|
|
991
|
+
name: "attach_volume",
|
|
992
|
+
description: "Attach a volume to a server",
|
|
993
|
+
inputSchema: {
|
|
994
|
+
type: "object",
|
|
995
|
+
properties: {
|
|
996
|
+
volumeId: {
|
|
997
|
+
type: "string",
|
|
998
|
+
description: "Volume ID",
|
|
999
|
+
},
|
|
1000
|
+
serverId: {
|
|
1001
|
+
type: "string",
|
|
1002
|
+
description: "Server ID",
|
|
1003
|
+
},
|
|
1004
|
+
automount: {
|
|
1005
|
+
type: "boolean",
|
|
1006
|
+
description: "Automatically mount the volume (default: true)",
|
|
1007
|
+
},
|
|
1008
|
+
},
|
|
1009
|
+
required: ["volumeId", "serverId"],
|
|
1010
|
+
},
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
name: "detach_volume",
|
|
1014
|
+
description: "Detach a volume from a server",
|
|
1015
|
+
inputSchema: {
|
|
1016
|
+
type: "object",
|
|
1017
|
+
properties: {
|
|
1018
|
+
volumeId: {
|
|
1019
|
+
type: "string",
|
|
1020
|
+
description: "Volume ID",
|
|
1021
|
+
},
|
|
1022
|
+
},
|
|
1023
|
+
required: ["volumeId"],
|
|
1024
|
+
},
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
name: "resize_volume",
|
|
1028
|
+
description: "Resize a volume (can only increase size)",
|
|
1029
|
+
inputSchema: {
|
|
1030
|
+
type: "object",
|
|
1031
|
+
properties: {
|
|
1032
|
+
volumeId: {
|
|
1033
|
+
type: "string",
|
|
1034
|
+
description: "Volume ID",
|
|
1035
|
+
},
|
|
1036
|
+
size: {
|
|
1037
|
+
type: "number",
|
|
1038
|
+
description: "New size in GB (must be larger than current)",
|
|
1039
|
+
},
|
|
1040
|
+
},
|
|
1041
|
+
required: ["volumeId", "size"],
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
name: "calculate_volume_price",
|
|
1046
|
+
description: "Calculate pricing for a volume based on size",
|
|
1047
|
+
inputSchema: {
|
|
1048
|
+
type: "object",
|
|
1049
|
+
properties: {
|
|
1050
|
+
size: {
|
|
1051
|
+
type: "number",
|
|
1052
|
+
description: "Volume size in GB",
|
|
1053
|
+
},
|
|
1054
|
+
},
|
|
1055
|
+
required: ["size"],
|
|
1056
|
+
},
|
|
1057
|
+
},
|
|
1058
|
+
// Resource Monitoring
|
|
1059
|
+
{
|
|
1060
|
+
name: "get_resources",
|
|
1061
|
+
description: "Get real-time CPU, memory, disk, GPU, network, and system metrics for a server via SSH",
|
|
1062
|
+
inputSchema: {
|
|
1063
|
+
type: "object",
|
|
1064
|
+
properties: {
|
|
1065
|
+
host: {
|
|
1066
|
+
type: "string",
|
|
1067
|
+
description: "Server IP or hostname",
|
|
1068
|
+
},
|
|
1069
|
+
user: {
|
|
1070
|
+
type: "string",
|
|
1071
|
+
description: "SSH user (default: root)",
|
|
1072
|
+
},
|
|
1073
|
+
keyPath: {
|
|
1074
|
+
type: "string",
|
|
1075
|
+
description: "Path to SSH private key (optional, uses default if not provided)",
|
|
1076
|
+
},
|
|
1077
|
+
password: {
|
|
1078
|
+
type: "string",
|
|
1079
|
+
description: "SSH password (optional, for password authentication)",
|
|
1080
|
+
},
|
|
1081
|
+
commands: {
|
|
1082
|
+
type: "array",
|
|
1083
|
+
description: "Resource commands to run (default: all). Options: cpu, memory, disk, gpu, network, loadavg, processes, connections, ports",
|
|
1084
|
+
items: {
|
|
1085
|
+
type: "string",
|
|
1086
|
+
},
|
|
1087
|
+
},
|
|
1088
|
+
},
|
|
1089
|
+
required: ["host"],
|
|
1090
|
+
},
|
|
1091
|
+
},
|
|
1092
|
+
],
|
|
1093
|
+
};
|
|
1094
|
+
});
|
|
1095
|
+
// Helper to get SSH options
|
|
1096
|
+
function getSSHOpts(host, user = "root", port = 22) {
|
|
1097
|
+
return {
|
|
1098
|
+
host,
|
|
1099
|
+
user,
|
|
1100
|
+
port,
|
|
1101
|
+
timeout: 30,
|
|
1102
|
+
keyPath: `${process.env.HOME}/.ssh/id_ed25519`,
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
// Handle tool calls
|
|
1106
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1107
|
+
const { name, arguments: args } = request.params;
|
|
1108
|
+
try {
|
|
1109
|
+
const hetzner = await getHetznerClient();
|
|
1110
|
+
switch (name) {
|
|
1111
|
+
// Server Management
|
|
1112
|
+
case "list_servers": {
|
|
1113
|
+
const servers = await hetzner.listServers();
|
|
1114
|
+
// Filter by status if provided
|
|
1115
|
+
const status = args?.status;
|
|
1116
|
+
const filtered = status ? servers.filter((s) => s.status === status) : servers;
|
|
1117
|
+
const lines = [
|
|
1118
|
+
`Hetzner Servers (${filtered.length} total)`,
|
|
1119
|
+
"=".repeat(50),
|
|
1120
|
+
"",
|
|
1121
|
+
];
|
|
1122
|
+
for (const server of filtered) {
|
|
1123
|
+
const status = server.status;
|
|
1124
|
+
const name = server.name;
|
|
1125
|
+
const id = server.id;
|
|
1126
|
+
const ipv4 = server.public_net?.ipv4?.ip || "no IP";
|
|
1127
|
+
const location = server.datacenter?.location?.name || "unknown";
|
|
1128
|
+
lines.push(`${name} (${status})`);
|
|
1129
|
+
lines.push(` ID: ${id}`);
|
|
1130
|
+
lines.push(` IP: ${ipv4}`);
|
|
1131
|
+
lines.push(` Location: ${location}`);
|
|
1132
|
+
lines.push("");
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
case "get_server": {
|
|
1139
|
+
const id = args.id;
|
|
1140
|
+
const server = await hetzner.getServer(Number(id));
|
|
1141
|
+
const lines = [
|
|
1142
|
+
`Server: ${server.name}`,
|
|
1143
|
+
"=".repeat(50),
|
|
1144
|
+
"",
|
|
1145
|
+
`ID: ${server.id}`,
|
|
1146
|
+
`Status: ${server.status}`,
|
|
1147
|
+
`Type: ${server.server_type?.name}`,
|
|
1148
|
+
`Image: ${server.image?.name} (${server.image?.description || ""})`,
|
|
1149
|
+
`Created: ${server.created}`,
|
|
1150
|
+
"",
|
|
1151
|
+
`Network:`,
|
|
1152
|
+
` IPv4: ${server.public_net?.ipv4?.ip || "none"}`,
|
|
1153
|
+
` IPv6: ${server.public_net?.ipv6?.ip || "none"}`,
|
|
1154
|
+
"",
|
|
1155
|
+
`Datacenter: ${server.datacenter?.description}`,
|
|
1156
|
+
`Location: ${server.datacenter?.location?.city}, ${server.datacenter?.location?.country}`,
|
|
1157
|
+
];
|
|
1158
|
+
return {
|
|
1159
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
case "get_server_types": {
|
|
1163
|
+
const types = await hetzner.pricing.listServerTypes();
|
|
1164
|
+
const lines = [
|
|
1165
|
+
`Hetzner Server Types (${types.length} available)`,
|
|
1166
|
+
"=".repeat(50),
|
|
1167
|
+
"",
|
|
1168
|
+
];
|
|
1169
|
+
for (const type of types.slice(0, 20)) {
|
|
1170
|
+
const cores = type.cores;
|
|
1171
|
+
const memory = type.memory;
|
|
1172
|
+
const disk = type.disk;
|
|
1173
|
+
const deprecated = type.deprecated ? " (deprecated)" : "";
|
|
1174
|
+
const price = type.prices?.[0]?.price_monthly?.gross || "N/A";
|
|
1175
|
+
lines.push(`${type.name}${deprecated}`);
|
|
1176
|
+
lines.push(` Cores: ${cores}, Memory: ${memory}MB, Disk: ${disk}GB`);
|
|
1177
|
+
lines.push(` Price: €${price}/month`);
|
|
1178
|
+
lines.push("");
|
|
1179
|
+
}
|
|
1180
|
+
return {
|
|
1181
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
case "get_datacenters": {
|
|
1185
|
+
const dcs = await hetzner.pricing.listDatacenters();
|
|
1186
|
+
const lines = [
|
|
1187
|
+
`Hetzner Datacenters (${dcs.length} total)`,
|
|
1188
|
+
"=".repeat(50),
|
|
1189
|
+
"",
|
|
1190
|
+
];
|
|
1191
|
+
for (const dc of dcs) {
|
|
1192
|
+
lines.push(`${dc.description}`);
|
|
1193
|
+
lines.push(` ID: ${dc.id}`);
|
|
1194
|
+
lines.push(` Location: ${dc.location?.city}, ${dc.location?.country}`);
|
|
1195
|
+
lines.push(` Supported Server Types: ${dc.supported_server_types?.length || 0}`);
|
|
1196
|
+
lines.push("");
|
|
1197
|
+
}
|
|
1198
|
+
return {
|
|
1199
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
case "get_locations": {
|
|
1203
|
+
const locations = await hetzner.pricing.listLocations();
|
|
1204
|
+
const lines = [
|
|
1205
|
+
`Hetzner Locations (${locations.length} total)`,
|
|
1206
|
+
"=".repeat(50),
|
|
1207
|
+
"",
|
|
1208
|
+
];
|
|
1209
|
+
for (const loc of locations) {
|
|
1210
|
+
lines.push(`${loc.name} (${loc.city}, ${loc.country})`);
|
|
1211
|
+
lines.push(` ID: ${loc.id}`);
|
|
1212
|
+
lines.push("");
|
|
1213
|
+
}
|
|
1214
|
+
return {
|
|
1215
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
case "get_ssh_keys": {
|
|
1219
|
+
const keys = await hetzner.ssh_keys.list();
|
|
1220
|
+
const lines = [
|
|
1221
|
+
`SSH Keys (${keys.length} total)`,
|
|
1222
|
+
"=".repeat(50),
|
|
1223
|
+
"",
|
|
1224
|
+
];
|
|
1225
|
+
for (const key of keys) {
|
|
1226
|
+
lines.push(`${key.name}`);
|
|
1227
|
+
lines.push(` ID: ${key.id}`);
|
|
1228
|
+
lines.push(` Fingerprint: ${key.fingerprint}`);
|
|
1229
|
+
lines.push(` Created: ${key.created}`);
|
|
1230
|
+
lines.push("");
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
case "create_server": {
|
|
1237
|
+
const params = args;
|
|
1238
|
+
const data = await hetzner.createServer({
|
|
1239
|
+
name: params.name,
|
|
1240
|
+
server_type: params.server_type,
|
|
1241
|
+
image: params.image,
|
|
1242
|
+
location: params.location,
|
|
1243
|
+
ssh_keys: params.ssh_keys,
|
|
1244
|
+
user_data: params.user_data,
|
|
1245
|
+
start_after_create: params.start_after_create ?? true,
|
|
1246
|
+
});
|
|
1247
|
+
const server = data.server;
|
|
1248
|
+
const nextActions = data.next_actions || [];
|
|
1249
|
+
const lines = [
|
|
1250
|
+
`Server creation initiated`,
|
|
1251
|
+
"=".repeat(50),
|
|
1252
|
+
"",
|
|
1253
|
+
`ID: ${server.id}`,
|
|
1254
|
+
`Name: ${server.name}`,
|
|
1255
|
+
`Status: ${server.status}`,
|
|
1256
|
+
"",
|
|
1257
|
+
`Next Actions:`,
|
|
1258
|
+
];
|
|
1259
|
+
for (const action of nextActions) {
|
|
1260
|
+
lines.push(` - ${action.command} (progress: ${action.progress}%)`);
|
|
1261
|
+
}
|
|
1262
|
+
lines.push("");
|
|
1263
|
+
lines.push(`Use 'get_actions ${server.id}' to track progress.`);
|
|
1264
|
+
return {
|
|
1265
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
case "create_server_hardened": {
|
|
1269
|
+
const params = args;
|
|
1270
|
+
// Generate cloud-init with security hardening
|
|
1271
|
+
const cloudInit = generateSeedBootstrap({
|
|
1272
|
+
enableSecurity: params.enable_security !== false,
|
|
1273
|
+
});
|
|
1274
|
+
const data = await hetzner.createServer({
|
|
1275
|
+
name: params.name,
|
|
1276
|
+
server_type: params.server_type,
|
|
1277
|
+
image: params.image,
|
|
1278
|
+
location: params.location,
|
|
1279
|
+
ssh_keys: params.ssh_keys,
|
|
1280
|
+
user_data: cloudInit,
|
|
1281
|
+
start_after_create: params.start_after_create ?? true,
|
|
1282
|
+
});
|
|
1283
|
+
const server = data.server;
|
|
1284
|
+
const nextActions = data.next_actions || [];
|
|
1285
|
+
const lines = [
|
|
1286
|
+
`🔒 Hardened server creation initiated`,
|
|
1287
|
+
"=".repeat(50),
|
|
1288
|
+
"",
|
|
1289
|
+
`ID: ${server.id}`,
|
|
1290
|
+
`Name: ${server.name}`,
|
|
1291
|
+
`Status: ${server.status}`,
|
|
1292
|
+
"",
|
|
1293
|
+
`Security Features:`,
|
|
1294
|
+
params.enable_security !== false ? ` ✓ SSH key-only authentication (no passwords)` : ` ✗ Security disabled`,
|
|
1295
|
+
params.enable_security !== false ? ` ✓ Fail2ban (3 failures → 1hr ban)` : ``,
|
|
1296
|
+
params.enable_security !== false ? ` ✓ UFW Firewall (default deny incoming)` : ``,
|
|
1297
|
+
params.enable_security !== false ? ` ✓ Kernel hardening` : ``,
|
|
1298
|
+
params.enable_security !== false ? ` ✓ SSH health monitoring` : ``,
|
|
1299
|
+
"",
|
|
1300
|
+
`Next Actions:`,
|
|
1301
|
+
];
|
|
1302
|
+
for (const action of nextActions) {
|
|
1303
|
+
lines.push(` - ${action.command} (progress: ${action.progress}%)`);
|
|
1304
|
+
}
|
|
1305
|
+
lines.push("");
|
|
1306
|
+
lines.push(`Server will be secured at first boot via cloud-init.`);
|
|
1307
|
+
lines.push(`Use 'get_actions ${server.id}' to track progress.`);
|
|
1308
|
+
return {
|
|
1309
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
case "delete_server": {
|
|
1313
|
+
const id = args.id;
|
|
1314
|
+
await hetzner.deleteServer(Number(id));
|
|
1315
|
+
return {
|
|
1316
|
+
content: [{ type: "text", text: `Server ${id} deletion initiated.` }],
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
case "power_on": {
|
|
1320
|
+
const id = args.id;
|
|
1321
|
+
const action = await hetzner.powerOn(Number(id));
|
|
1322
|
+
return {
|
|
1323
|
+
content: [{ type: "text", text: `Power on initiated for ${id}\nAction ID: ${action.id}` }],
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
case "power_off": {
|
|
1327
|
+
const id = args.id;
|
|
1328
|
+
const action = await hetzner.powerOff(Number(id));
|
|
1329
|
+
return {
|
|
1330
|
+
content: [{ type: "text", text: `Power off initiated for ${id}\nAction ID: ${action.id}` }],
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
case "reboot": {
|
|
1334
|
+
const id = args.id;
|
|
1335
|
+
const action = await hetzner.reboot(Number(id));
|
|
1336
|
+
return {
|
|
1337
|
+
content: [{ type: "text", text: `Reboot initiated for ${id}\nAction ID: ${action.id}` }],
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
case "get_actions": {
|
|
1341
|
+
const id = args.id;
|
|
1342
|
+
const actions = await hetzner.actions.list({ server_id: Number(id) });
|
|
1343
|
+
const lines = [
|
|
1344
|
+
`Actions for ${id} (${actions.length} recent)`,
|
|
1345
|
+
"=".repeat(50),
|
|
1346
|
+
"",
|
|
1347
|
+
];
|
|
1348
|
+
for (const action of actions.slice(0, 10)) {
|
|
1349
|
+
const status = action.status;
|
|
1350
|
+
const command = action.command;
|
|
1351
|
+
const started = action.started;
|
|
1352
|
+
const finished = action.finished;
|
|
1353
|
+
lines.push(`${command} (${status})`);
|
|
1354
|
+
lines.push(` ID: ${action.id}`);
|
|
1355
|
+
lines.push(` Started: ${started}`);
|
|
1356
|
+
if (finished)
|
|
1357
|
+
lines.push(` Finished: ${finished}`);
|
|
1358
|
+
if (action.error)
|
|
1359
|
+
lines.push(` Error: ${action.error}`);
|
|
1360
|
+
lines.push(` Progress: ${action.progress}%`);
|
|
1361
|
+
lines.push("");
|
|
1362
|
+
}
|
|
1363
|
+
return {
|
|
1364
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
// SSH Operations
|
|
1368
|
+
case "ssh_exec": {
|
|
1369
|
+
const { host, command, user = "root", port = 22 } = args;
|
|
1370
|
+
const sshOpts = getSSHOpts(host, user, port);
|
|
1371
|
+
const result = await execSSH(command, sshOpts);
|
|
1372
|
+
return {
|
|
1373
|
+
content: [{ type: "text", text: result || "Command executed" }],
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
case "ssh_test": {
|
|
1377
|
+
const { host, user = "root", port = 22 } = args;
|
|
1378
|
+
const sshOpts = getSSHOpts(host, user, port);
|
|
1379
|
+
const result = await testSSHConnection(sshOpts);
|
|
1380
|
+
return {
|
|
1381
|
+
content: [{ type: "text", text: result ? "SSH connection successful" : "SSH connection failed" }],
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
case "ssh_exec_parallel": {
|
|
1385
|
+
const { hosts, command, user = "root" } = args;
|
|
1386
|
+
// Execute command on all hosts in parallel using Promise.all
|
|
1387
|
+
const results = {};
|
|
1388
|
+
await Promise.all(hosts.map(async (host) => {
|
|
1389
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1390
|
+
try {
|
|
1391
|
+
results[host] = await execSSH(command, sshOpts);
|
|
1392
|
+
}
|
|
1393
|
+
catch (error) {
|
|
1394
|
+
results[host] = `Error: ${error.message}`;
|
|
1395
|
+
}
|
|
1396
|
+
}));
|
|
1397
|
+
const lines = ["Parallel SSH Execution Results", "=".repeat(50), ""];
|
|
1398
|
+
for (const [host, output] of Object.entries(results)) {
|
|
1399
|
+
const status = output && !output.startsWith("Error:") ? "SUCCESS" : "FAILED";
|
|
1400
|
+
lines.push(`${host}: ${status}`);
|
|
1401
|
+
if (output)
|
|
1402
|
+
lines.push(` ${output}`);
|
|
1403
|
+
lines.push("");
|
|
1404
|
+
}
|
|
1405
|
+
return {
|
|
1406
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
// Tmux Operations
|
|
1410
|
+
case "tmux_create_or_attach": {
|
|
1411
|
+
const { host, sessionName, user = "root" } = args;
|
|
1412
|
+
// Note: sessionName parameter is not currently supported by @ebowwa/terminal
|
|
1413
|
+
// The function generates session name automatically from host and user
|
|
1414
|
+
const result = await createOrAttachTmuxSession(host, user, undefined, {});
|
|
1415
|
+
return {
|
|
1416
|
+
content: [{ type: "text", text: result.sessionName ? `Session: ${result.sessionName}\nSSH: ssh ${result.sshArgs.join(" ")}` : "Failed to create session" }],
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
case "tmux_list": {
|
|
1420
|
+
const { host, user = "root" } = args;
|
|
1421
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1422
|
+
const sessions = await listTmuxSessions(sshOpts);
|
|
1423
|
+
const lines = [`Tmux sessions on ${host}:`, ...sessions.map((s) => ` - ${s}`), ""];
|
|
1424
|
+
return {
|
|
1425
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
case "tmux_kill": {
|
|
1429
|
+
const { host, sessionName, user = "root" } = args;
|
|
1430
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1431
|
+
const killed = await killTmuxSession(sessionName, sshOpts);
|
|
1432
|
+
return {
|
|
1433
|
+
content: [{ type: "text", text: killed ? `Session '${sessionName}' killed` : `Failed to kill session '${sessionName}'` }],
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
case "tmux_send_command": {
|
|
1437
|
+
const { host, sessionName, command, paneIndex = "0", user = "root" } = args;
|
|
1438
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1439
|
+
const sent = await sendCommandToPane(sessionName, command, paneIndex, sshOpts);
|
|
1440
|
+
return {
|
|
1441
|
+
content: [{ type: "text", text: sent ? "Command sent" : "Failed to send command" }],
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
case "tmux_capture": {
|
|
1445
|
+
const { host, sessionName, paneIndex = "0", user = "root" } = args;
|
|
1446
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1447
|
+
const output = await capturePane(sessionName, paneIndex, sshOpts);
|
|
1448
|
+
return {
|
|
1449
|
+
content: [{ type: "text", text: output || "No output captured" }],
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
case "tmux_cleanup": {
|
|
1453
|
+
const { host, ageLimit = 30, user = "root" } = args;
|
|
1454
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1455
|
+
const ageMs = ageLimit * 24 * 60 * 60 * 1000;
|
|
1456
|
+
const result = await cleanupOldTmuxSessions(sshOpts, { sessionAgeLimit: ageMs });
|
|
1457
|
+
const lines = [
|
|
1458
|
+
`Cleanup on ${host}:`,
|
|
1459
|
+
` Cleaned: ${result.cleaned} sessions`,
|
|
1460
|
+
result.errors?.length ? ` Errors: ${result.errors.join(", ")}` : "",
|
|
1461
|
+
];
|
|
1462
|
+
return {
|
|
1463
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
case "tmux_list_windows": {
|
|
1467
|
+
const { host, sessionName, user = "root" } = args;
|
|
1468
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1469
|
+
const windows = await listSessionWindows(sessionName, sshOpts);
|
|
1470
|
+
const lines = [
|
|
1471
|
+
`Windows in session '${sessionName}' on ${host}:`,
|
|
1472
|
+
"=".repeat(50),
|
|
1473
|
+
"",
|
|
1474
|
+
];
|
|
1475
|
+
for (const win of windows) {
|
|
1476
|
+
lines.push(`Window ${win.index}: ${win.name}${win.active ? " (active)" : ""}`);
|
|
1477
|
+
}
|
|
1478
|
+
return {
|
|
1479
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
case "tmux_list_panes": {
|
|
1483
|
+
const { host, sessionName, windowIndex = "0", user = "root" } = args;
|
|
1484
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1485
|
+
const panes = await listWindowPanes(sessionName, windowIndex, sshOpts);
|
|
1486
|
+
const lines = [
|
|
1487
|
+
`Panes in window ${windowIndex} of '${sessionName}' on ${host}:`,
|
|
1488
|
+
"=".repeat(50),
|
|
1489
|
+
"",
|
|
1490
|
+
];
|
|
1491
|
+
for (const pane of panes) {
|
|
1492
|
+
lines.push(`Pane ${pane.index}: PID ${pane.pid}${pane.active ? " (active)" : ""}`);
|
|
1493
|
+
lines.push(` Path: ${pane.currentPath || "none"}`);
|
|
1494
|
+
lines.push("");
|
|
1495
|
+
}
|
|
1496
|
+
return {
|
|
1497
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
case "tmux_split_pane": {
|
|
1501
|
+
const { host, sessionName, windowIndex = "0", paneIndex = "0", direction = "h", user = "root" } = args;
|
|
1502
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1503
|
+
await splitPane(sessionName, direction, null, sshOpts);
|
|
1504
|
+
return {
|
|
1505
|
+
content: [{ type: "text", text: `Split pane in window ${windowIndex} of session '${sessionName}' (${direction === "h" ? "horizontal" : "vertical"})` }],
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
case "tmux_kill_pane": {
|
|
1509
|
+
const { host, sessionName, paneIndex, user = "root" } = args;
|
|
1510
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1511
|
+
await killPane(sessionName, paneIndex, sshOpts);
|
|
1512
|
+
return {
|
|
1513
|
+
content: [{ type: "text", text: `Killed pane ${paneIndex} in session '${sessionName}'` }],
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
case "tmux_switch_window": {
|
|
1517
|
+
const { host, sessionName, windowIndex, user = "root" } = args;
|
|
1518
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1519
|
+
await switchWindow(sessionName, windowIndex, sshOpts);
|
|
1520
|
+
return {
|
|
1521
|
+
content: [{ type: "text", text: `Switched to window ${windowIndex} in session '${sessionName}'` }],
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
case "tmux_switch_pane": {
|
|
1525
|
+
const { host, sessionName, paneIndex, user = "root" } = args;
|
|
1526
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1527
|
+
await switchPane(sessionName, paneIndex, sshOpts);
|
|
1528
|
+
return {
|
|
1529
|
+
content: [{ type: "text", text: `Switched to pane ${paneIndex} in session '${sessionName}'` }],
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
case "tmux_rename_window": {
|
|
1533
|
+
const { host, sessionName, windowIndex = "0", newName, user = "root" } = args;
|
|
1534
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1535
|
+
await renameWindow(sessionName, windowIndex, newName, sshOpts);
|
|
1536
|
+
return {
|
|
1537
|
+
content: [{ type: "text", text: `Renamed window ${windowIndex} to '${newName}' in session '${sessionName}'` }],
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
// File Operations
|
|
1541
|
+
case "scp_upload": {
|
|
1542
|
+
const { host, source, destination, user = "root" } = args;
|
|
1543
|
+
await scpUpload({ host, source, destination, user, timeout: 30 });
|
|
1544
|
+
return {
|
|
1545
|
+
content: [{ type: "text", text: `Uploaded ${source} to ${host}:${destination}` }],
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
case "scp_download": {
|
|
1549
|
+
const { host, source, destination, user = "root" } = args;
|
|
1550
|
+
await scpDownload({ host, source, destination, user, timeout: 30 });
|
|
1551
|
+
return {
|
|
1552
|
+
content: [{ type: "text", text: `Downloaded ${host}:${source} to ${destination}` }],
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
case "file_list": {
|
|
1556
|
+
const { host, path = ".", user = "root" } = args;
|
|
1557
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1558
|
+
const files = await listFiles(path === "." ? undefined : path, sshOpts);
|
|
1559
|
+
const lines = [
|
|
1560
|
+
`Files in ${path || "."} on ${host}:`,
|
|
1561
|
+
"=".repeat(50),
|
|
1562
|
+
...files.map(f => `${f.type}: ${f.path} (${f.size || 0} bytes)`),
|
|
1563
|
+
"",
|
|
1564
|
+
];
|
|
1565
|
+
return {
|
|
1566
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
case "file_preview": {
|
|
1570
|
+
const { host, path, user = "root" } = args;
|
|
1571
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1572
|
+
const preview = await previewFile(path, sshOpts);
|
|
1573
|
+
return {
|
|
1574
|
+
content: [{ type: "text", text: preview.content || preview.error || "Could not preview file" }],
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
// Security Verification Tools
|
|
1578
|
+
case "check_bootstrap_status": {
|
|
1579
|
+
const { host, user = "root" } = args;
|
|
1580
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1581
|
+
const status = await execSSH("cat /root/.bootstrap-status 2>/dev/null || echo 'No bootstrap status file'", sshOpts);
|
|
1582
|
+
const lines = [
|
|
1583
|
+
`📋 Bootstrap Status for ${host}`,
|
|
1584
|
+
"=".repeat(50),
|
|
1585
|
+
"",
|
|
1586
|
+
status,
|
|
1587
|
+
"",
|
|
1588
|
+
];
|
|
1589
|
+
return {
|
|
1590
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
case "check_security_status": {
|
|
1594
|
+
const { host, user = "root" } = args;
|
|
1595
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1596
|
+
const commands = [
|
|
1597
|
+
'echo "=== UFW Firewall ==="',
|
|
1598
|
+
'ufw status verbose',
|
|
1599
|
+
'echo ""',
|
|
1600
|
+
'echo "=== Fail2ban ==="',
|
|
1601
|
+
'systemctl status fail2ban --no-pager | head -3',
|
|
1602
|
+
'fail2ban-client status sshd 2>/dev/null | grep -E "(Status|banned|IP)"',
|
|
1603
|
+
'echo ""',
|
|
1604
|
+
'echo "=== SSH Hardening ==="',
|
|
1605
|
+
'sshd -T 2>/dev/null | grep -E "(PasswordAuth|PermitRootLogin|MaxStartups)"',
|
|
1606
|
+
'echo ""',
|
|
1607
|
+
'echo "=== Kernel Hardening ==="',
|
|
1608
|
+
'sysctl -n net.ipv4.conf.all.rp_filter net.ipv4.tcp_syncookies kernel.randomize_va_space 2>/dev/null',
|
|
1609
|
+
];
|
|
1610
|
+
const results = await execSSH(commands.join(" && "), sshOpts);
|
|
1611
|
+
const lines = [
|
|
1612
|
+
`🔒 Security Status for ${host}`,
|
|
1613
|
+
"=".repeat(50),
|
|
1614
|
+
"",
|
|
1615
|
+
results,
|
|
1616
|
+
"",
|
|
1617
|
+
];
|
|
1618
|
+
return {
|
|
1619
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
case "get_ssh_logs": {
|
|
1623
|
+
const { host, lines = 20, user = "root" } = args;
|
|
1624
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1625
|
+
const logs = await execSSH(`journalctl -u ssh --no-pager -n ${lines} | grep -E "(Accepted|Failed|Postponed|session)"`, sshOpts);
|
|
1626
|
+
const output = logs.trim() || "No recent SSH logs";
|
|
1627
|
+
const text = [
|
|
1628
|
+
`📝 SSH Logs for ${host} (last ${lines} entries)`,
|
|
1629
|
+
"=".repeat(50),
|
|
1630
|
+
"",
|
|
1631
|
+
output,
|
|
1632
|
+
"",
|
|
1633
|
+
];
|
|
1634
|
+
return {
|
|
1635
|
+
content: [{ type: "text", text: text.join("\n") }],
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
case "check_fail2ban": {
|
|
1639
|
+
const { host, user = "root" } = args;
|
|
1640
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1641
|
+
const results = await execSSH(`
|
|
1642
|
+
echo "=== Fail2ban Status ==="
|
|
1643
|
+
systemctl is-active fail2ban
|
|
1644
|
+
echo ""
|
|
1645
|
+
echo "=== SSH Jail Status ==="
|
|
1646
|
+
fail2ban-client status sshd
|
|
1647
|
+
echo ""
|
|
1648
|
+
echo "=== Failed Logins (last hour) ==="
|
|
1649
|
+
journalctl -u ssh --since "1 hour ago" --no-pager | grep -c "Failed" || echo "0"
|
|
1650
|
+
`, sshOpts);
|
|
1651
|
+
const text = [
|
|
1652
|
+
`🚫 Fail2ban Status for ${host}`,
|
|
1653
|
+
"=".repeat(50),
|
|
1654
|
+
"",
|
|
1655
|
+
results,
|
|
1656
|
+
"",
|
|
1657
|
+
];
|
|
1658
|
+
return {
|
|
1659
|
+
content: [{ type: "text", text: text.join("\n") }],
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
case "check_server_health": {
|
|
1663
|
+
const { host, user = "root" } = args;
|
|
1664
|
+
const sshOpts = getSSHOpts(host, user);
|
|
1665
|
+
const health = await execSSH("cat /var/log/sshd-health.json 2>/dev/null || echo 'No health data yet'", sshOpts);
|
|
1666
|
+
const lines = [
|
|
1667
|
+
`💓 Server Health for ${host}`,
|
|
1668
|
+
"=".repeat(50),
|
|
1669
|
+
"",
|
|
1670
|
+
health,
|
|
1671
|
+
"",
|
|
1672
|
+
];
|
|
1673
|
+
return {
|
|
1674
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
// Volume Management
|
|
1678
|
+
case "list_volumes": {
|
|
1679
|
+
const volumes = await hetzner.volumes.list({
|
|
1680
|
+
name: args?.name,
|
|
1681
|
+
status: args?.status,
|
|
1682
|
+
});
|
|
1683
|
+
const lines = [
|
|
1684
|
+
`💾 Hetzner Volumes (${volumes.length} total)`,
|
|
1685
|
+
"=".repeat(50),
|
|
1686
|
+
"",
|
|
1687
|
+
];
|
|
1688
|
+
for (const volume of volumes) {
|
|
1689
|
+
const status = volume.status;
|
|
1690
|
+
const name = volume.name;
|
|
1691
|
+
const id = volume.id;
|
|
1692
|
+
const size = volume.size;
|
|
1693
|
+
const server = volume.server || "not attached";
|
|
1694
|
+
const location = volume.location?.name || "unknown";
|
|
1695
|
+
lines.push(`${name} (${status})`);
|
|
1696
|
+
lines.push(` ID: ${id}`);
|
|
1697
|
+
lines.push(` Size: ${size} GB`);
|
|
1698
|
+
lines.push(` Server: ${server}`);
|
|
1699
|
+
lines.push(` Location: ${location}`);
|
|
1700
|
+
lines.push("");
|
|
1701
|
+
}
|
|
1702
|
+
return {
|
|
1703
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
case "get_volume": {
|
|
1707
|
+
const id = parseInt(args.id);
|
|
1708
|
+
const volume = await hetzner.volumes.get(id);
|
|
1709
|
+
const lines = [
|
|
1710
|
+
`💾 Volume: ${volume.name}`,
|
|
1711
|
+
"=".repeat(50),
|
|
1712
|
+
"",
|
|
1713
|
+
`ID: ${volume.id}`,
|
|
1714
|
+
`Status: ${volume.status}`,
|
|
1715
|
+
`Size: ${volume.size} GB`,
|
|
1716
|
+
`Location: ${volume.location?.name || "unknown"}`,
|
|
1717
|
+
`Server: ${volume.server || "not attached"}`,
|
|
1718
|
+
`Format: ${volume.format || "not formatted"}`,
|
|
1719
|
+
`Automount: ${volume.automount ? "yes" : "no"}`,
|
|
1720
|
+
`Protection: ${volume.protection?.delete ? "enabled" : "disabled"}`,
|
|
1721
|
+
`Labels: ${JSON.stringify(volume.labels || {})}`,
|
|
1722
|
+
"",
|
|
1723
|
+
`Created: ${volume.created}`,
|
|
1724
|
+
];
|
|
1725
|
+
return {
|
|
1726
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
case "create_volume": {
|
|
1730
|
+
const { name, size, server, location, automount, format } = args;
|
|
1731
|
+
const result = await hetzner.volumes.create({
|
|
1732
|
+
name,
|
|
1733
|
+
size,
|
|
1734
|
+
server: server ? parseInt(server) : undefined,
|
|
1735
|
+
location,
|
|
1736
|
+
automount: automount ?? true,
|
|
1737
|
+
format,
|
|
1738
|
+
});
|
|
1739
|
+
const lines = [
|
|
1740
|
+
"✅ Volume created successfully",
|
|
1741
|
+
"=".repeat(50),
|
|
1742
|
+
"",
|
|
1743
|
+
`Name: ${result.volume.name}`,
|
|
1744
|
+
`ID: ${result.volume.id}`,
|
|
1745
|
+
`Size: ${result.volume.size} GB`,
|
|
1746
|
+
`Status: ${result.volume.status}`,
|
|
1747
|
+
"",
|
|
1748
|
+
"Action:",
|
|
1749
|
+
` ID: ${result.action.id}`,
|
|
1750
|
+
` Status: ${result.action.status}`,
|
|
1751
|
+
` Command: ${result.action.command}`,
|
|
1752
|
+
];
|
|
1753
|
+
return {
|
|
1754
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
case "delete_volume": {
|
|
1758
|
+
const id = parseInt(args.id);
|
|
1759
|
+
const action = await hetzner.volumes.delete(id);
|
|
1760
|
+
const lines = [
|
|
1761
|
+
"🗑️ Volume deletion initiated",
|
|
1762
|
+
"=".repeat(50),
|
|
1763
|
+
"",
|
|
1764
|
+
`Action ID: ${action.id}`,
|
|
1765
|
+
`Status: ${action.status}`,
|
|
1766
|
+
`Resource: ${action.resources?.[0]?.id || "unknown"}`,
|
|
1767
|
+
`Resource Type: ${action.resources?.[0]?.type || "unknown"}`,
|
|
1768
|
+
];
|
|
1769
|
+
return {
|
|
1770
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
case "attach_volume": {
|
|
1774
|
+
const { volumeId, serverId, automount } = args;
|
|
1775
|
+
const action = await hetzner.volumes.attach(parseInt(volumeId), parseInt(serverId), automount ?? true);
|
|
1776
|
+
const lines = [
|
|
1777
|
+
"🔌 Volume attachment initiated",
|
|
1778
|
+
"=".repeat(50),
|
|
1779
|
+
"",
|
|
1780
|
+
`Action ID: ${action.id}`,
|
|
1781
|
+
`Status: ${action.status}`,
|
|
1782
|
+
`Volume ID: ${action.resources?.[0]?.id || volumeId}`,
|
|
1783
|
+
`Server ID: ${action.resources?.[1]?.id || serverId}`,
|
|
1784
|
+
];
|
|
1785
|
+
return {
|
|
1786
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
case "detach_volume": {
|
|
1790
|
+
const volumeId = parseInt(args.volumeId);
|
|
1791
|
+
const action = await hetzner.volumes.detach(volumeId);
|
|
1792
|
+
const lines = [
|
|
1793
|
+
"🔌 Volume detachment initiated",
|
|
1794
|
+
"=".repeat(50),
|
|
1795
|
+
"",
|
|
1796
|
+
`Action ID: ${action.id}`,
|
|
1797
|
+
`Status: ${action.status}`,
|
|
1798
|
+
`Volume ID: ${action.resources?.[0]?.id || volumeId.toString()}`,
|
|
1799
|
+
];
|
|
1800
|
+
return {
|
|
1801
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
case "resize_volume": {
|
|
1805
|
+
const { volumeId, size } = args;
|
|
1806
|
+
const action = await hetzner.volumes.resize(parseInt(volumeId), size);
|
|
1807
|
+
const lines = [
|
|
1808
|
+
"📏 Volume resize initiated",
|
|
1809
|
+
"=".repeat(50),
|
|
1810
|
+
"",
|
|
1811
|
+
`Action ID: ${action.id}`,
|
|
1812
|
+
`Status: ${action.status}`,
|
|
1813
|
+
`Volume ID: ${action.resources?.[0]?.id || volumeId}`,
|
|
1814
|
+
`New Size: ${size} GB`,
|
|
1815
|
+
];
|
|
1816
|
+
return {
|
|
1817
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
case "calculate_volume_price": {
|
|
1821
|
+
const size = args.size;
|
|
1822
|
+
const price = VolumeOperations.calculatePrice(size);
|
|
1823
|
+
const lines = [
|
|
1824
|
+
`💰 Volume Pricing (${size} GB)`,
|
|
1825
|
+
"=".repeat(50),
|
|
1826
|
+
"",
|
|
1827
|
+
`Monthly: €${price.monthly}/month`,
|
|
1828
|
+
`Hourly: €${price.hourly}/hour`,
|
|
1829
|
+
`Currency: ${price.currency}`,
|
|
1830
|
+
"",
|
|
1831
|
+
`Price per GB: €0.008/month`,
|
|
1832
|
+
];
|
|
1833
|
+
return {
|
|
1834
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
// Resource Monitoring
|
|
1838
|
+
case "get_resources": {
|
|
1839
|
+
const { host, user = "root", keyPath, password, commands } = args;
|
|
1840
|
+
// Default to all commands if none specified
|
|
1841
|
+
const allCommands = Object.keys(RESOURCE_COMMANDS);
|
|
1842
|
+
const commandsToRun = commands || allCommands;
|
|
1843
|
+
// Build command object with only requested commands
|
|
1844
|
+
const selectedCommands = {};
|
|
1845
|
+
for (const cmd of commandsToRun) {
|
|
1846
|
+
if (cmd in RESOURCE_COMMANDS) {
|
|
1847
|
+
selectedCommands[cmd] = RESOURCE_COMMANDS[cmd];
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
const sshOpts = {
|
|
1851
|
+
host,
|
|
1852
|
+
user,
|
|
1853
|
+
timeout: 10,
|
|
1854
|
+
...(keyPath && { keyPath }),
|
|
1855
|
+
...(password && { password }),
|
|
1856
|
+
};
|
|
1857
|
+
// Execute commands in parallel using Promise.all
|
|
1858
|
+
const rawResults = {};
|
|
1859
|
+
await Promise.all(Object.entries(selectedCommands).map(async ([name, command]) => {
|
|
1860
|
+
try {
|
|
1861
|
+
rawResults[name] = await execSSH(command, sshOpts);
|
|
1862
|
+
}
|
|
1863
|
+
catch {
|
|
1864
|
+
rawResults[name] = "";
|
|
1865
|
+
}
|
|
1866
|
+
}));
|
|
1867
|
+
const resources = parseResources(rawResults);
|
|
1868
|
+
const lines = [
|
|
1869
|
+
`📊 Resources for ${host}`,
|
|
1870
|
+
"=".repeat(50),
|
|
1871
|
+
"",
|
|
1872
|
+
`CPU: ${resources.cpuPercent}%`,
|
|
1873
|
+
`Memory: ${resources.memoryPercent}% (${resources.memoryUsed} / ${resources.memoryTotal})`,
|
|
1874
|
+
`Disk: ${resources.diskPercent}% (${resources.diskUsed} / ${resources.diskTotal})`,
|
|
1875
|
+
...(resources.gpuPercent !== undefined ? [`GPU: ${resources.gpuPercent}% (${resources.gpuMemoryUsed} / ${resources.gpuMemoryTotal})`] : []),
|
|
1876
|
+
...(resources.loadavg ? [`Load: ${resources.loadavg}`] : []),
|
|
1877
|
+
...(resources.processes ? [`Processes: ${resources.processes}`] : []),
|
|
1878
|
+
...(resources.connections ? [`Connections: ${resources.connections}`] : []),
|
|
1879
|
+
...(resources.ports ? [`Ports: ${resources.ports}`] : []),
|
|
1880
|
+
"",
|
|
1881
|
+
];
|
|
1882
|
+
return {
|
|
1883
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
default:
|
|
1887
|
+
return {
|
|
1888
|
+
content: [
|
|
1889
|
+
{
|
|
1890
|
+
type: "text",
|
|
1891
|
+
text: `Unknown tool: ${name}`,
|
|
1892
|
+
},
|
|
1893
|
+
],
|
|
1894
|
+
isError: true,
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
catch (error) {
|
|
1899
|
+
return {
|
|
1900
|
+
content: [
|
|
1901
|
+
{
|
|
1902
|
+
type: "text",
|
|
1903
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
1904
|
+
},
|
|
1905
|
+
],
|
|
1906
|
+
isError: true,
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
// Start server
|
|
1911
|
+
async function main() {
|
|
1912
|
+
const token = process.env.HETZNER_API_TOKEN || await getTokenFromCLI();
|
|
1913
|
+
if (token) {
|
|
1914
|
+
hetznerClient = new HetznerClient(token);
|
|
1915
|
+
console.error("[@mcp/hetzner] Authenticated via", process.env.HETZNER_API_TOKEN ? "HETZNER_API_TOKEN" : "Hetzner CLI config");
|
|
1916
|
+
}
|
|
1917
|
+
else {
|
|
1918
|
+
console.warn("[@mcp/hetzner] HETZNER_API_TOKEN not configured - MCP will run in limited mode");
|
|
1919
|
+
}
|
|
1920
|
+
const transport = new StdioServerTransport();
|
|
1921
|
+
await server.connect(transport);
|
|
1922
|
+
console.error("[@mcp/hetzner] Server running on stdio");
|
|
1923
|
+
}
|
|
1924
|
+
main().catch((error) => {
|
|
1925
|
+
console.error("Fatal error:", error);
|
|
1926
|
+
process.exit(1);
|
|
1927
|
+
});
|
|
1928
|
+
//# sourceMappingURL=index.js.map
|