myaidev-method 0.2.19 → 0.2.22
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/.claude/mcp/sparc-orchestrator-server.js +0 -0
- package/.claude/mcp/wordpress-server.js +0 -0
- package/CHANGELOG.md +123 -5
- package/README.md +205 -13
- package/TECHNICAL_ARCHITECTURE.md +64 -2
- package/bin/cli.js +169 -2
- package/dist/mcp/mcp-config.json +138 -1
- package/dist/mcp/openstack-server.js +1607 -0
- package/package.json +2 -2
- package/src/config/workflows.js +532 -0
- package/src/lib/payloadcms-utils.js +206 -0
- package/src/lib/visual-generation-utils.js +445 -294
- package/src/lib/workflow-installer.js +512 -0
- package/src/libs/security/authorization-checker.js +606 -0
- package/src/mcp/openstack-server.js +1607 -0
- package/src/scripts/openstack-setup.sh +110 -0
- package/src/scripts/security/environment-detect.js +425 -0
- package/src/templates/claude/agents/openstack-vm-manager.md +281 -0
- package/src/templates/claude/agents/osint-researcher.md +1075 -0
- package/src/templates/claude/agents/penetration-tester.md +908 -0
- package/src/templates/claude/agents/security-auditor.md +244 -0
- package/src/templates/claude/agents/security-setup.md +1094 -0
- package/src/templates/claude/agents/webapp-security-tester.md +581 -0
- package/src/templates/claude/commands/myai-configure.md +84 -0
- package/src/templates/claude/commands/myai-openstack.md +229 -0
- package/src/templates/claude/commands/sc:security-exploit.md +464 -0
- package/src/templates/claude/commands/sc:security-recon.md +281 -0
- package/src/templates/claude/commands/sc:security-report.md +756 -0
- package/src/templates/claude/commands/sc:security-scan.md +441 -0
- package/src/templates/claude/commands/sc:security-setup.md +501 -0
- package/src/templates/claude/mcp_config.json +44 -0
|
@@ -0,0 +1,1607 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { spawn, execSync } from "child_process";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import https from "https";
|
|
9
|
+
import http from "http";
|
|
10
|
+
import dotenv from "dotenv";
|
|
11
|
+
|
|
12
|
+
// Load environment variables
|
|
13
|
+
dotenv.config();
|
|
14
|
+
|
|
15
|
+
// OpenStack environment variables
|
|
16
|
+
const OS_AUTH_URL = process.env.OS_AUTH_URL;
|
|
17
|
+
const OS_USERNAME = process.env.OS_USERNAME;
|
|
18
|
+
const OS_PASSWORD = process.env.OS_PASSWORD;
|
|
19
|
+
const OS_PROJECT_ID = process.env.OS_PROJECT_ID;
|
|
20
|
+
const OS_USER_DOMAIN_ID = process.env.OS_USER_DOMAIN_ID || "default";
|
|
21
|
+
const OS_PROJECT_DOMAIN_ID = process.env.OS_PROJECT_DOMAIN_ID || "default";
|
|
22
|
+
const OS_REGION_NAME = process.env.OS_REGION_NAME;
|
|
23
|
+
const OS_IDENTITY_API_VERSION = process.env.OS_IDENTITY_API_VERSION || "3";
|
|
24
|
+
|
|
25
|
+
// Default cloud-init URL from environment
|
|
26
|
+
const DEFAULT_CLOUD_INIT = process.env.CLOUD_INIT;
|
|
27
|
+
|
|
28
|
+
// Create MCP server
|
|
29
|
+
const server = new McpServer({
|
|
30
|
+
name: "openstack-mcp-server",
|
|
31
|
+
version: "1.0.0",
|
|
32
|
+
description: "OpenStack MCP Server for VM management and orchestration"
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Session storage for tracking operations
|
|
36
|
+
const sessions = new Map();
|
|
37
|
+
const operationHistory = [];
|
|
38
|
+
|
|
39
|
+
// Helper to generate session IDs
|
|
40
|
+
function generateSessionId() {
|
|
41
|
+
return `os-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Build OpenStack environment for subprocess
|
|
45
|
+
function getOpenStackEnv() {
|
|
46
|
+
return {
|
|
47
|
+
...process.env,
|
|
48
|
+
OS_AUTH_URL,
|
|
49
|
+
OS_USERNAME,
|
|
50
|
+
OS_PASSWORD,
|
|
51
|
+
OS_PROJECT_ID,
|
|
52
|
+
OS_USER_DOMAIN_ID,
|
|
53
|
+
OS_PROJECT_DOMAIN_ID,
|
|
54
|
+
OS_REGION_NAME,
|
|
55
|
+
OS_IDENTITY_API_VERSION,
|
|
56
|
+
OS_AUTH_VERSION: OS_IDENTITY_API_VERSION,
|
|
57
|
+
OS_ENDPOINT_TYPE: "publicURL",
|
|
58
|
+
OS_INTERFACE: "publicURL",
|
|
59
|
+
OS_NO_CACHE: "1"
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Execute OpenStack CLI command
|
|
64
|
+
async function runOpenStackCommand(args, timeout = 120000) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const env = getOpenStackEnv();
|
|
67
|
+
|
|
68
|
+
// Check for required environment variables
|
|
69
|
+
if (!OS_AUTH_URL || !OS_USERNAME || !OS_PASSWORD || !OS_PROJECT_ID) {
|
|
70
|
+
reject(new Error("Missing required OpenStack environment variables. Please configure using /myai-configure openstack"));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const proc = spawn("openstack", args, {
|
|
75
|
+
env,
|
|
76
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let stdout = "";
|
|
80
|
+
let stderr = "";
|
|
81
|
+
|
|
82
|
+
proc.stdout.on("data", (data) => {
|
|
83
|
+
stdout += data.toString();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
proc.stderr.on("data", (data) => {
|
|
87
|
+
stderr += data.toString();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const timeoutId = setTimeout(() => {
|
|
91
|
+
proc.kill("SIGTERM");
|
|
92
|
+
reject(new Error(`Command timed out after ${timeout}ms`));
|
|
93
|
+
}, timeout);
|
|
94
|
+
|
|
95
|
+
proc.on("close", (code) => {
|
|
96
|
+
clearTimeout(timeoutId);
|
|
97
|
+
if (code === 0) {
|
|
98
|
+
resolve({ success: true, output: stdout.trim(), stderr: stderr.trim() });
|
|
99
|
+
} else {
|
|
100
|
+
reject(new Error(stderr || `Command failed with exit code ${code}`));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
proc.on("error", (error) => {
|
|
105
|
+
clearTimeout(timeoutId);
|
|
106
|
+
reject(new Error(`Failed to execute command: ${error.message}`));
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Parse OpenStack table output to JSON
|
|
112
|
+
function parseTableOutput(output) {
|
|
113
|
+
const lines = output.split("\n").filter(line => line.trim() && !line.startsWith("+"));
|
|
114
|
+
if (lines.length < 2) return [];
|
|
115
|
+
|
|
116
|
+
const headers = lines[0].split("|")
|
|
117
|
+
.map(h => h.trim().toLowerCase().replace(/\s+/g, "_"))
|
|
118
|
+
.filter(h => h);
|
|
119
|
+
|
|
120
|
+
return lines.slice(1).map(line => {
|
|
121
|
+
const values = line.split("|").map(v => v.trim()).filter(v => v !== "");
|
|
122
|
+
const obj = {};
|
|
123
|
+
headers.forEach((header, i) => {
|
|
124
|
+
obj[header] = values[i] || "";
|
|
125
|
+
});
|
|
126
|
+
return obj;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Fetch content from URL (supports GitHub Gist raw URLs)
|
|
131
|
+
async function fetchFromUrl(url) {
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
// Convert GitHub Gist URL to raw URL if needed
|
|
134
|
+
let fetchUrl = url;
|
|
135
|
+
if (url.includes('gist.github.com') && !url.includes('/raw')) {
|
|
136
|
+
// Extract gist ID and convert to raw URL
|
|
137
|
+
const gistMatch = url.match(/gist\.github\.com\/([^\/]+)\/([a-f0-9]+)/);
|
|
138
|
+
if (gistMatch) {
|
|
139
|
+
fetchUrl = `https://gist.githubusercontent.com/${gistMatch[1]}/${gistMatch[2]}/raw`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const protocol = fetchUrl.startsWith('https') ? https : http;
|
|
144
|
+
|
|
145
|
+
const request = protocol.get(fetchUrl, {
|
|
146
|
+
headers: {
|
|
147
|
+
'User-Agent': 'MyAIDev-Method-OpenStack-MCP/1.0'
|
|
148
|
+
}
|
|
149
|
+
}, (response) => {
|
|
150
|
+
// Handle redirects
|
|
151
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
152
|
+
fetchFromUrl(response.headers.location).then(resolve).catch(reject);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (response.statusCode !== 200) {
|
|
157
|
+
reject(new Error(`HTTP ${response.statusCode}: Failed to fetch ${fetchUrl}`));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let data = '';
|
|
162
|
+
response.on('data', chunk => data += chunk);
|
|
163
|
+
response.on('end', () => resolve(data));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
request.on('error', reject);
|
|
167
|
+
request.setTimeout(30000, () => {
|
|
168
|
+
request.destroy();
|
|
169
|
+
reject(new Error('Request timeout'));
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Resolve cloud-init from various sources
|
|
175
|
+
async function resolveCloudInit(params) {
|
|
176
|
+
// Priority: explicit user_data > cloud_init_url > cloud_init_file > use_default_cloud_init > none
|
|
177
|
+
|
|
178
|
+
// 1. Explicit user_data content provided
|
|
179
|
+
if (params.user_data) {
|
|
180
|
+
return { content: params.user_data, source: 'inline' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 2. Cloud-init URL provided
|
|
184
|
+
if (params.cloud_init_url) {
|
|
185
|
+
try {
|
|
186
|
+
const content = await fetchFromUrl(params.cloud_init_url);
|
|
187
|
+
return { content, source: `url:${params.cloud_init_url}` };
|
|
188
|
+
} catch (error) {
|
|
189
|
+
throw new Error(`Failed to fetch cloud-init from URL: ${error.message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 3. Cloud-init file path provided
|
|
194
|
+
if (params.cloud_init_file) {
|
|
195
|
+
try {
|
|
196
|
+
const content = fs.readFileSync(params.cloud_init_file, 'utf8');
|
|
197
|
+
return { content, source: `file:${params.cloud_init_file}` };
|
|
198
|
+
} catch (error) {
|
|
199
|
+
throw new Error(`Failed to read cloud-init file: ${error.message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 4. Use default cloud-init from environment if requested
|
|
204
|
+
if (params.use_default_cloud_init && DEFAULT_CLOUD_INIT) {
|
|
205
|
+
try {
|
|
206
|
+
// Check if it's a URL or file path
|
|
207
|
+
if (DEFAULT_CLOUD_INIT.startsWith('http://') || DEFAULT_CLOUD_INIT.startsWith('https://')) {
|
|
208
|
+
const content = await fetchFromUrl(DEFAULT_CLOUD_INIT);
|
|
209
|
+
return { content, source: `default_url:${DEFAULT_CLOUD_INIT}` };
|
|
210
|
+
} else {
|
|
211
|
+
const content = fs.readFileSync(DEFAULT_CLOUD_INIT, 'utf8');
|
|
212
|
+
return { content, source: `default_file:${DEFAULT_CLOUD_INIT}` };
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
throw new Error(`Failed to load default cloud-init: ${error.message}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// No cloud-init
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Cloud-init fetch and preview tool
|
|
224
|
+
server.registerTool("os_cloud_init_fetch", {
|
|
225
|
+
title: "Fetch Cloud-Init Configuration",
|
|
226
|
+
description: "Fetch and preview cloud-init configuration from URL, file, or default",
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: "object",
|
|
229
|
+
properties: {
|
|
230
|
+
url: {
|
|
231
|
+
type: "string",
|
|
232
|
+
description: "URL to fetch cloud-init from (supports GitHub Gist URLs)"
|
|
233
|
+
},
|
|
234
|
+
file: {
|
|
235
|
+
type: "string",
|
|
236
|
+
description: "Local file path to cloud-init script"
|
|
237
|
+
},
|
|
238
|
+
use_default: {
|
|
239
|
+
type: "boolean",
|
|
240
|
+
description: "Fetch from the default CLOUD_INIT environment variable",
|
|
241
|
+
default: false
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
additionalProperties: false
|
|
245
|
+
}
|
|
246
|
+
}, async (params) => {
|
|
247
|
+
try {
|
|
248
|
+
let content = null;
|
|
249
|
+
let source = null;
|
|
250
|
+
|
|
251
|
+
if (params.url) {
|
|
252
|
+
content = await fetchFromUrl(params.url);
|
|
253
|
+
source = `url:${params.url}`;
|
|
254
|
+
} else if (params.file) {
|
|
255
|
+
content = fs.readFileSync(params.file, 'utf8');
|
|
256
|
+
source = `file:${params.file}`;
|
|
257
|
+
} else if (params.use_default && DEFAULT_CLOUD_INIT) {
|
|
258
|
+
if (DEFAULT_CLOUD_INIT.startsWith('http://') || DEFAULT_CLOUD_INIT.startsWith('https://')) {
|
|
259
|
+
content = await fetchFromUrl(DEFAULT_CLOUD_INIT);
|
|
260
|
+
source = `default_url:${DEFAULT_CLOUD_INIT}`;
|
|
261
|
+
} else {
|
|
262
|
+
content = fs.readFileSync(DEFAULT_CLOUD_INIT, 'utf8');
|
|
263
|
+
source = `default_file:${DEFAULT_CLOUD_INIT}`;
|
|
264
|
+
}
|
|
265
|
+
} else if (params.use_default && !DEFAULT_CLOUD_INIT) {
|
|
266
|
+
return {
|
|
267
|
+
content: [{
|
|
268
|
+
type: "text",
|
|
269
|
+
text: JSON.stringify({
|
|
270
|
+
success: false,
|
|
271
|
+
error: "No default CLOUD_INIT configured in environment"
|
|
272
|
+
}, null, 2)
|
|
273
|
+
}]
|
|
274
|
+
};
|
|
275
|
+
} else {
|
|
276
|
+
return {
|
|
277
|
+
content: [{
|
|
278
|
+
type: "text",
|
|
279
|
+
text: JSON.stringify({
|
|
280
|
+
success: false,
|
|
281
|
+
error: "Please provide url, file, or set use_default to true"
|
|
282
|
+
}, null, 2)
|
|
283
|
+
}]
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
content: [{
|
|
289
|
+
type: "text",
|
|
290
|
+
text: JSON.stringify({
|
|
291
|
+
success: true,
|
|
292
|
+
source,
|
|
293
|
+
content_length: content.length,
|
|
294
|
+
preview: content.substring(0, 2000) + (content.length > 2000 ? '\n... (truncated)' : ''),
|
|
295
|
+
full_content: content
|
|
296
|
+
}, null, 2)
|
|
297
|
+
}]
|
|
298
|
+
};
|
|
299
|
+
} catch (error) {
|
|
300
|
+
return {
|
|
301
|
+
content: [{
|
|
302
|
+
type: "text",
|
|
303
|
+
text: JSON.stringify({
|
|
304
|
+
success: false,
|
|
305
|
+
error: error.message
|
|
306
|
+
}, null, 2)
|
|
307
|
+
}]
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Get default cloud-init info
|
|
313
|
+
server.registerTool("os_cloud_init_info", {
|
|
314
|
+
title: "Cloud-Init Info",
|
|
315
|
+
description: "Get information about configured cloud-init defaults",
|
|
316
|
+
inputSchema: {
|
|
317
|
+
type: "object",
|
|
318
|
+
properties: {},
|
|
319
|
+
additionalProperties: false
|
|
320
|
+
}
|
|
321
|
+
}, async () => {
|
|
322
|
+
return {
|
|
323
|
+
content: [{
|
|
324
|
+
type: "text",
|
|
325
|
+
text: JSON.stringify({
|
|
326
|
+
success: true,
|
|
327
|
+
default_cloud_init: DEFAULT_CLOUD_INIT || null,
|
|
328
|
+
configured: !!DEFAULT_CLOUD_INIT,
|
|
329
|
+
type: DEFAULT_CLOUD_INIT ?
|
|
330
|
+
(DEFAULT_CLOUD_INIT.startsWith('http') ? 'url' : 'file') :
|
|
331
|
+
null,
|
|
332
|
+
usage: {
|
|
333
|
+
use_default: "Set use_default_cloud_init: true when creating a server",
|
|
334
|
+
custom_url: "Provide cloud_init_url parameter with any URL",
|
|
335
|
+
custom_file: "Provide cloud_init_file parameter with local path",
|
|
336
|
+
inline: "Provide user_data parameter with YAML content"
|
|
337
|
+
}
|
|
338
|
+
}, null, 2)
|
|
339
|
+
}]
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Session management tool
|
|
344
|
+
server.registerTool("os_session_create", {
|
|
345
|
+
title: "Create OpenStack Session",
|
|
346
|
+
description: "Create a new session for tracking OpenStack operations",
|
|
347
|
+
inputSchema: {
|
|
348
|
+
type: "object",
|
|
349
|
+
properties: {
|
|
350
|
+
description: {
|
|
351
|
+
type: "string",
|
|
352
|
+
description: "Description of the session purpose"
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
additionalProperties: false
|
|
356
|
+
}
|
|
357
|
+
}, async (params) => {
|
|
358
|
+
const sessionId = generateSessionId();
|
|
359
|
+
sessions.set(sessionId, {
|
|
360
|
+
id: sessionId,
|
|
361
|
+
description: params.description || "OpenStack operations session",
|
|
362
|
+
created: new Date().toISOString(),
|
|
363
|
+
operations: []
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
content: [{
|
|
368
|
+
type: "text",
|
|
369
|
+
text: JSON.stringify({
|
|
370
|
+
success: true,
|
|
371
|
+
session_id: sessionId,
|
|
372
|
+
message: "Session created successfully"
|
|
373
|
+
}, null, 2)
|
|
374
|
+
}]
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Health check tool
|
|
379
|
+
server.registerTool("os_health_check", {
|
|
380
|
+
title: "OpenStack Health Check",
|
|
381
|
+
description: "Check OpenStack API connectivity and authentication",
|
|
382
|
+
inputSchema: {
|
|
383
|
+
type: "object",
|
|
384
|
+
properties: {},
|
|
385
|
+
additionalProperties: false
|
|
386
|
+
}
|
|
387
|
+
}, async () => {
|
|
388
|
+
try {
|
|
389
|
+
const result = await runOpenStackCommand(["token", "issue", "-f", "json"]);
|
|
390
|
+
const tokenInfo = JSON.parse(result.output);
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
content: [{
|
|
394
|
+
type: "text",
|
|
395
|
+
text: JSON.stringify({
|
|
396
|
+
success: true,
|
|
397
|
+
message: "OpenStack API is responding",
|
|
398
|
+
auth_url: OS_AUTH_URL,
|
|
399
|
+
project_id: OS_PROJECT_ID,
|
|
400
|
+
region: OS_REGION_NAME,
|
|
401
|
+
token_expires: tokenInfo.expires,
|
|
402
|
+
server_version: "1.0.0"
|
|
403
|
+
}, null, 2)
|
|
404
|
+
}]
|
|
405
|
+
};
|
|
406
|
+
} catch (error) {
|
|
407
|
+
return {
|
|
408
|
+
content: [{
|
|
409
|
+
type: "text",
|
|
410
|
+
text: JSON.stringify({
|
|
411
|
+
success: false,
|
|
412
|
+
error: error.message,
|
|
413
|
+
suggestion: "Check your OpenStack credentials. Run /myai-configure openstack to reconfigure."
|
|
414
|
+
}, null, 2)
|
|
415
|
+
}]
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// List available images
|
|
421
|
+
server.registerTool("os_image_list", {
|
|
422
|
+
title: "List OpenStack Images",
|
|
423
|
+
description: "List available VM images in OpenStack",
|
|
424
|
+
inputSchema: {
|
|
425
|
+
type: "object",
|
|
426
|
+
properties: {
|
|
427
|
+
limit: {
|
|
428
|
+
type: "number",
|
|
429
|
+
description: "Maximum number of images to return",
|
|
430
|
+
default: 20
|
|
431
|
+
},
|
|
432
|
+
status: {
|
|
433
|
+
type: "string",
|
|
434
|
+
enum: ["active", "queued", "saving", "deleted"],
|
|
435
|
+
description: "Filter by image status"
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
additionalProperties: false
|
|
439
|
+
}
|
|
440
|
+
}, async (params) => {
|
|
441
|
+
try {
|
|
442
|
+
const args = ["image", "list", "-f", "json"];
|
|
443
|
+
if (params.limit) args.push("--limit", params.limit.toString());
|
|
444
|
+
if (params.status) args.push("--status", params.status);
|
|
445
|
+
|
|
446
|
+
const result = await runOpenStackCommand(args);
|
|
447
|
+
const images = JSON.parse(result.output);
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
content: [{
|
|
451
|
+
type: "text",
|
|
452
|
+
text: JSON.stringify({
|
|
453
|
+
success: true,
|
|
454
|
+
count: images.length,
|
|
455
|
+
images: images.map(img => ({
|
|
456
|
+
id: img.ID,
|
|
457
|
+
name: img.Name,
|
|
458
|
+
status: img.Status
|
|
459
|
+
}))
|
|
460
|
+
}, null, 2)
|
|
461
|
+
}]
|
|
462
|
+
};
|
|
463
|
+
} catch (error) {
|
|
464
|
+
return {
|
|
465
|
+
content: [{
|
|
466
|
+
type: "text",
|
|
467
|
+
text: JSON.stringify({
|
|
468
|
+
success: false,
|
|
469
|
+
error: error.message
|
|
470
|
+
}, null, 2)
|
|
471
|
+
}]
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// List available flavors
|
|
477
|
+
server.registerTool("os_flavor_list", {
|
|
478
|
+
title: "List OpenStack Flavors",
|
|
479
|
+
description: "List available VM flavors (instance sizes) in OpenStack",
|
|
480
|
+
inputSchema: {
|
|
481
|
+
type: "object",
|
|
482
|
+
properties: {},
|
|
483
|
+
additionalProperties: false
|
|
484
|
+
}
|
|
485
|
+
}, async () => {
|
|
486
|
+
try {
|
|
487
|
+
const result = await runOpenStackCommand(["flavor", "list", "-f", "json"]);
|
|
488
|
+
const flavors = JSON.parse(result.output);
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
content: [{
|
|
492
|
+
type: "text",
|
|
493
|
+
text: JSON.stringify({
|
|
494
|
+
success: true,
|
|
495
|
+
count: flavors.length,
|
|
496
|
+
flavors: flavors.map(f => ({
|
|
497
|
+
id: f.ID,
|
|
498
|
+
name: f.Name,
|
|
499
|
+
vcpus: f.VCPUs,
|
|
500
|
+
ram_mb: f.RAM,
|
|
501
|
+
disk_gb: f.Disk
|
|
502
|
+
}))
|
|
503
|
+
}, null, 2)
|
|
504
|
+
}]
|
|
505
|
+
};
|
|
506
|
+
} catch (error) {
|
|
507
|
+
return {
|
|
508
|
+
content: [{
|
|
509
|
+
type: "text",
|
|
510
|
+
text: JSON.stringify({
|
|
511
|
+
success: false,
|
|
512
|
+
error: error.message
|
|
513
|
+
}, null, 2)
|
|
514
|
+
}]
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// List available networks
|
|
520
|
+
server.registerTool("os_network_list", {
|
|
521
|
+
title: "List OpenStack Networks",
|
|
522
|
+
description: "List available networks in OpenStack",
|
|
523
|
+
inputSchema: {
|
|
524
|
+
type: "object",
|
|
525
|
+
properties: {},
|
|
526
|
+
additionalProperties: false
|
|
527
|
+
}
|
|
528
|
+
}, async () => {
|
|
529
|
+
try {
|
|
530
|
+
const result = await runOpenStackCommand(["network", "list", "-f", "json"]);
|
|
531
|
+
const networks = JSON.parse(result.output);
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
content: [{
|
|
535
|
+
type: "text",
|
|
536
|
+
text: JSON.stringify({
|
|
537
|
+
success: true,
|
|
538
|
+
count: networks.length,
|
|
539
|
+
networks: networks.map(n => ({
|
|
540
|
+
id: n.ID,
|
|
541
|
+
name: n.Name,
|
|
542
|
+
subnets: n.Subnets
|
|
543
|
+
}))
|
|
544
|
+
}, null, 2)
|
|
545
|
+
}]
|
|
546
|
+
};
|
|
547
|
+
} catch (error) {
|
|
548
|
+
return {
|
|
549
|
+
content: [{
|
|
550
|
+
type: "text",
|
|
551
|
+
text: JSON.stringify({
|
|
552
|
+
success: false,
|
|
553
|
+
error: error.message
|
|
554
|
+
}, null, 2)
|
|
555
|
+
}]
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// List security groups
|
|
561
|
+
server.registerTool("os_security_group_list", {
|
|
562
|
+
title: "List Security Groups",
|
|
563
|
+
description: "List available security groups in OpenStack",
|
|
564
|
+
inputSchema: {
|
|
565
|
+
type: "object",
|
|
566
|
+
properties: {},
|
|
567
|
+
additionalProperties: false
|
|
568
|
+
}
|
|
569
|
+
}, async () => {
|
|
570
|
+
try {
|
|
571
|
+
const result = await runOpenStackCommand(["security", "group", "list", "-f", "json"]);
|
|
572
|
+
const groups = JSON.parse(result.output);
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
content: [{
|
|
576
|
+
type: "text",
|
|
577
|
+
text: JSON.stringify({
|
|
578
|
+
success: true,
|
|
579
|
+
count: groups.length,
|
|
580
|
+
security_groups: groups.map(g => ({
|
|
581
|
+
id: g.ID,
|
|
582
|
+
name: g.Name,
|
|
583
|
+
description: g.Description
|
|
584
|
+
}))
|
|
585
|
+
}, null, 2)
|
|
586
|
+
}]
|
|
587
|
+
};
|
|
588
|
+
} catch (error) {
|
|
589
|
+
return {
|
|
590
|
+
content: [{
|
|
591
|
+
type: "text",
|
|
592
|
+
text: JSON.stringify({
|
|
593
|
+
success: false,
|
|
594
|
+
error: error.message
|
|
595
|
+
}, null, 2)
|
|
596
|
+
}]
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// List keypairs
|
|
602
|
+
server.registerTool("os_keypair_list", {
|
|
603
|
+
title: "List SSH Keypairs",
|
|
604
|
+
description: "List available SSH keypairs in OpenStack",
|
|
605
|
+
inputSchema: {
|
|
606
|
+
type: "object",
|
|
607
|
+
properties: {},
|
|
608
|
+
additionalProperties: false
|
|
609
|
+
}
|
|
610
|
+
}, async () => {
|
|
611
|
+
try {
|
|
612
|
+
const result = await runOpenStackCommand(["keypair", "list", "-f", "json"]);
|
|
613
|
+
const keypairs = JSON.parse(result.output);
|
|
614
|
+
|
|
615
|
+
return {
|
|
616
|
+
content: [{
|
|
617
|
+
type: "text",
|
|
618
|
+
text: JSON.stringify({
|
|
619
|
+
success: true,
|
|
620
|
+
count: keypairs.length,
|
|
621
|
+
keypairs: keypairs.map(k => ({
|
|
622
|
+
name: k.Name,
|
|
623
|
+
fingerprint: k.Fingerprint,
|
|
624
|
+
type: k.Type
|
|
625
|
+
}))
|
|
626
|
+
}, null, 2)
|
|
627
|
+
}]
|
|
628
|
+
};
|
|
629
|
+
} catch (error) {
|
|
630
|
+
return {
|
|
631
|
+
content: [{
|
|
632
|
+
type: "text",
|
|
633
|
+
text: JSON.stringify({
|
|
634
|
+
success: false,
|
|
635
|
+
error: error.message
|
|
636
|
+
}, null, 2)
|
|
637
|
+
}]
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// Create keypair
|
|
643
|
+
server.registerTool("os_keypair_create", {
|
|
644
|
+
title: "Create SSH Keypair",
|
|
645
|
+
description: "Create a new SSH keypair for VM access",
|
|
646
|
+
inputSchema: {
|
|
647
|
+
type: "object",
|
|
648
|
+
properties: {
|
|
649
|
+
name: {
|
|
650
|
+
type: "string",
|
|
651
|
+
description: "Name for the keypair"
|
|
652
|
+
},
|
|
653
|
+
public_key_file: {
|
|
654
|
+
type: "string",
|
|
655
|
+
description: "Path to existing public key file (optional, will generate if not provided)"
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
required: ["name"],
|
|
659
|
+
additionalProperties: false
|
|
660
|
+
}
|
|
661
|
+
}, async (params) => {
|
|
662
|
+
try {
|
|
663
|
+
const args = ["keypair", "create", params.name];
|
|
664
|
+
|
|
665
|
+
if (params.public_key_file) {
|
|
666
|
+
args.push("--public-key", params.public_key_file);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
args.push("-f", "json");
|
|
670
|
+
|
|
671
|
+
const result = await runOpenStackCommand(args);
|
|
672
|
+
const keypair = JSON.parse(result.output);
|
|
673
|
+
|
|
674
|
+
const response = {
|
|
675
|
+
success: true,
|
|
676
|
+
keypair: {
|
|
677
|
+
name: keypair.name || params.name,
|
|
678
|
+
fingerprint: keypair.fingerprint
|
|
679
|
+
},
|
|
680
|
+
message: "Keypair created successfully"
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// If a new keypair was generated (no public key provided), include the private key
|
|
684
|
+
if (!params.public_key_file && keypair.private_key) {
|
|
685
|
+
response.private_key = keypair.private_key;
|
|
686
|
+
response.warning = "Save this private key securely. It will not be shown again!";
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return {
|
|
690
|
+
content: [{
|
|
691
|
+
type: "text",
|
|
692
|
+
text: JSON.stringify(response, null, 2)
|
|
693
|
+
}]
|
|
694
|
+
};
|
|
695
|
+
} catch (error) {
|
|
696
|
+
return {
|
|
697
|
+
content: [{
|
|
698
|
+
type: "text",
|
|
699
|
+
text: JSON.stringify({
|
|
700
|
+
success: false,
|
|
701
|
+
error: error.message
|
|
702
|
+
}, null, 2)
|
|
703
|
+
}]
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// List servers (VMs)
|
|
709
|
+
server.registerTool("os_server_list", {
|
|
710
|
+
title: "List Servers",
|
|
711
|
+
description: "List all VMs/servers in the project",
|
|
712
|
+
inputSchema: {
|
|
713
|
+
type: "object",
|
|
714
|
+
properties: {
|
|
715
|
+
status: {
|
|
716
|
+
type: "string",
|
|
717
|
+
enum: ["ACTIVE", "BUILD", "ERROR", "SHUTOFF", "SUSPENDED", "PAUSED"],
|
|
718
|
+
description: "Filter by server status"
|
|
719
|
+
},
|
|
720
|
+
name: {
|
|
721
|
+
type: "string",
|
|
722
|
+
description: "Filter by server name (partial match)"
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
additionalProperties: false
|
|
726
|
+
}
|
|
727
|
+
}, async (params) => {
|
|
728
|
+
try {
|
|
729
|
+
const args = ["server", "list", "-f", "json"];
|
|
730
|
+
if (params.status) args.push("--status", params.status);
|
|
731
|
+
if (params.name) args.push("--name", params.name);
|
|
732
|
+
|
|
733
|
+
const result = await runOpenStackCommand(args);
|
|
734
|
+
const servers = JSON.parse(result.output);
|
|
735
|
+
|
|
736
|
+
return {
|
|
737
|
+
content: [{
|
|
738
|
+
type: "text",
|
|
739
|
+
text: JSON.stringify({
|
|
740
|
+
success: true,
|
|
741
|
+
count: servers.length,
|
|
742
|
+
servers: servers.map(s => ({
|
|
743
|
+
id: s.ID,
|
|
744
|
+
name: s.Name,
|
|
745
|
+
status: s.Status,
|
|
746
|
+
networks: s.Networks,
|
|
747
|
+
image: s.Image,
|
|
748
|
+
flavor: s.Flavor
|
|
749
|
+
}))
|
|
750
|
+
}, null, 2)
|
|
751
|
+
}]
|
|
752
|
+
};
|
|
753
|
+
} catch (error) {
|
|
754
|
+
return {
|
|
755
|
+
content: [{
|
|
756
|
+
type: "text",
|
|
757
|
+
text: JSON.stringify({
|
|
758
|
+
success: false,
|
|
759
|
+
error: error.message
|
|
760
|
+
}, null, 2)
|
|
761
|
+
}]
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// Create server (VM)
|
|
767
|
+
server.registerTool("os_server_create", {
|
|
768
|
+
title: "Create Server",
|
|
769
|
+
description: "Create a new VM/server in OpenStack with optional cloud-init configuration",
|
|
770
|
+
inputSchema: {
|
|
771
|
+
type: "object",
|
|
772
|
+
properties: {
|
|
773
|
+
name: {
|
|
774
|
+
type: "string",
|
|
775
|
+
description: "Name for the new server"
|
|
776
|
+
},
|
|
777
|
+
image: {
|
|
778
|
+
type: "string",
|
|
779
|
+
description: "Image ID or name to use"
|
|
780
|
+
},
|
|
781
|
+
flavor: {
|
|
782
|
+
type: "string",
|
|
783
|
+
description: "Flavor ID or name (instance size)"
|
|
784
|
+
},
|
|
785
|
+
network: {
|
|
786
|
+
type: "string",
|
|
787
|
+
description: "Network ID or name to attach"
|
|
788
|
+
},
|
|
789
|
+
keypair: {
|
|
790
|
+
type: "string",
|
|
791
|
+
description: "SSH keypair name for access"
|
|
792
|
+
},
|
|
793
|
+
security_groups: {
|
|
794
|
+
type: "array",
|
|
795
|
+
items: { type: "string" },
|
|
796
|
+
description: "Security group names to apply"
|
|
797
|
+
},
|
|
798
|
+
user_data: {
|
|
799
|
+
type: "string",
|
|
800
|
+
description: "Inline cloud-init user data script (plain text YAML)"
|
|
801
|
+
},
|
|
802
|
+
cloud_init_url: {
|
|
803
|
+
type: "string",
|
|
804
|
+
description: "URL to fetch cloud-init script from (supports GitHub Gist URLs)"
|
|
805
|
+
},
|
|
806
|
+
cloud_init_file: {
|
|
807
|
+
type: "string",
|
|
808
|
+
description: "Local file path to cloud-init script"
|
|
809
|
+
},
|
|
810
|
+
use_default_cloud_init: {
|
|
811
|
+
type: "boolean",
|
|
812
|
+
description: "Use the default cloud-init from CLOUD_INIT environment variable",
|
|
813
|
+
default: false
|
|
814
|
+
},
|
|
815
|
+
availability_zone: {
|
|
816
|
+
type: "string",
|
|
817
|
+
description: "Availability zone for the server"
|
|
818
|
+
},
|
|
819
|
+
wait: {
|
|
820
|
+
type: "boolean",
|
|
821
|
+
description: "Wait for server to become active",
|
|
822
|
+
default: true
|
|
823
|
+
}
|
|
824
|
+
},
|
|
825
|
+
required: ["name", "image", "flavor"],
|
|
826
|
+
additionalProperties: false
|
|
827
|
+
}
|
|
828
|
+
}, async (params) => {
|
|
829
|
+
let tmpFile = null;
|
|
830
|
+
let cloudInitInfo = null;
|
|
831
|
+
|
|
832
|
+
try {
|
|
833
|
+
const args = ["server", "create"];
|
|
834
|
+
args.push("--image", params.image);
|
|
835
|
+
args.push("--flavor", params.flavor);
|
|
836
|
+
|
|
837
|
+
if (params.network) {
|
|
838
|
+
args.push("--network", params.network);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (params.keypair) {
|
|
842
|
+
args.push("--key-name", params.keypair);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (params.security_groups && params.security_groups.length > 0) {
|
|
846
|
+
params.security_groups.forEach(sg => {
|
|
847
|
+
args.push("--security-group", sg);
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Resolve cloud-init from various sources
|
|
852
|
+
cloudInitInfo = await resolveCloudInit(params);
|
|
853
|
+
if (cloudInitInfo) {
|
|
854
|
+
tmpFile = `/tmp/userdata-${Date.now()}.txt`;
|
|
855
|
+
fs.writeFileSync(tmpFile, cloudInitInfo.content);
|
|
856
|
+
args.push("--user-data", tmpFile);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (params.availability_zone) {
|
|
860
|
+
args.push("--availability-zone", params.availability_zone);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (params.wait !== false) {
|
|
864
|
+
args.push("--wait");
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
args.push("-f", "json");
|
|
868
|
+
args.push(params.name);
|
|
869
|
+
|
|
870
|
+
const result = await runOpenStackCommand(args, 300000); // 5 minute timeout for server creation
|
|
871
|
+
const serverResult = JSON.parse(result.output);
|
|
872
|
+
|
|
873
|
+
// Log operation
|
|
874
|
+
operationHistory.push({
|
|
875
|
+
type: "server_create",
|
|
876
|
+
server_id: serverResult.id,
|
|
877
|
+
server_name: params.name,
|
|
878
|
+
cloud_init_source: cloudInitInfo ? cloudInitInfo.source : null,
|
|
879
|
+
timestamp: new Date().toISOString()
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
const response = {
|
|
883
|
+
success: true,
|
|
884
|
+
server: {
|
|
885
|
+
id: serverResult.id,
|
|
886
|
+
name: serverResult.name,
|
|
887
|
+
status: serverResult.status,
|
|
888
|
+
addresses: serverResult.addresses,
|
|
889
|
+
image: serverResult.image,
|
|
890
|
+
flavor: serverResult.flavor,
|
|
891
|
+
key_name: serverResult.key_name,
|
|
892
|
+
security_groups: serverResult.security_groups
|
|
893
|
+
},
|
|
894
|
+
message: "Server created successfully"
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
// Include cloud-init info in response
|
|
898
|
+
if (cloudInitInfo) {
|
|
899
|
+
response.cloud_init = {
|
|
900
|
+
source: cloudInitInfo.source,
|
|
901
|
+
applied: true
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
content: [{
|
|
907
|
+
type: "text",
|
|
908
|
+
text: JSON.stringify(response, null, 2)
|
|
909
|
+
}]
|
|
910
|
+
};
|
|
911
|
+
} catch (error) {
|
|
912
|
+
return {
|
|
913
|
+
content: [{
|
|
914
|
+
type: "text",
|
|
915
|
+
text: JSON.stringify({
|
|
916
|
+
success: false,
|
|
917
|
+
error: error.message
|
|
918
|
+
}, null, 2)
|
|
919
|
+
}]
|
|
920
|
+
};
|
|
921
|
+
} finally {
|
|
922
|
+
// Clean up temp file
|
|
923
|
+
if (tmpFile) {
|
|
924
|
+
try {
|
|
925
|
+
fs.unlinkSync(tmpFile);
|
|
926
|
+
} catch (e) {}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// Get server details
|
|
932
|
+
server.registerTool("os_server_show", {
|
|
933
|
+
title: "Show Server Details",
|
|
934
|
+
description: "Get detailed information about a specific server",
|
|
935
|
+
inputSchema: {
|
|
936
|
+
type: "object",
|
|
937
|
+
properties: {
|
|
938
|
+
server: {
|
|
939
|
+
type: "string",
|
|
940
|
+
description: "Server ID or name"
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
required: ["server"],
|
|
944
|
+
additionalProperties: false
|
|
945
|
+
}
|
|
946
|
+
}, async (params) => {
|
|
947
|
+
try {
|
|
948
|
+
const result = await runOpenStackCommand(["server", "show", params.server, "-f", "json"]);
|
|
949
|
+
const server = JSON.parse(result.output);
|
|
950
|
+
|
|
951
|
+
return {
|
|
952
|
+
content: [{
|
|
953
|
+
type: "text",
|
|
954
|
+
text: JSON.stringify({
|
|
955
|
+
success: true,
|
|
956
|
+
server: {
|
|
957
|
+
id: server.id,
|
|
958
|
+
name: server.name,
|
|
959
|
+
status: server.status,
|
|
960
|
+
addresses: server.addresses,
|
|
961
|
+
image: server.image,
|
|
962
|
+
flavor: server.flavor,
|
|
963
|
+
key_name: server.key_name,
|
|
964
|
+
security_groups: server.security_groups,
|
|
965
|
+
created: server.created,
|
|
966
|
+
updated: server.updated,
|
|
967
|
+
availability_zone: server.availability_zone,
|
|
968
|
+
host: server["OS-EXT-SRV-ATTR:host"],
|
|
969
|
+
power_state: server["OS-EXT-STS:power_state"],
|
|
970
|
+
task_state: server["OS-EXT-STS:task_state"]
|
|
971
|
+
}
|
|
972
|
+
}, null, 2)
|
|
973
|
+
}]
|
|
974
|
+
};
|
|
975
|
+
} catch (error) {
|
|
976
|
+
return {
|
|
977
|
+
content: [{
|
|
978
|
+
type: "text",
|
|
979
|
+
text: JSON.stringify({
|
|
980
|
+
success: false,
|
|
981
|
+
error: error.message
|
|
982
|
+
}, null, 2)
|
|
983
|
+
}]
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
// Delete server
|
|
989
|
+
server.registerTool("os_server_delete", {
|
|
990
|
+
title: "Delete Server",
|
|
991
|
+
description: "Delete a VM/server from OpenStack",
|
|
992
|
+
inputSchema: {
|
|
993
|
+
type: "object",
|
|
994
|
+
properties: {
|
|
995
|
+
server: {
|
|
996
|
+
type: "string",
|
|
997
|
+
description: "Server ID or name to delete"
|
|
998
|
+
},
|
|
999
|
+
wait: {
|
|
1000
|
+
type: "boolean",
|
|
1001
|
+
description: "Wait for deletion to complete",
|
|
1002
|
+
default: true
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
required: ["server"],
|
|
1006
|
+
additionalProperties: false
|
|
1007
|
+
}
|
|
1008
|
+
}, async (params) => {
|
|
1009
|
+
try {
|
|
1010
|
+
const args = ["server", "delete", params.server];
|
|
1011
|
+
if (params.wait !== false) {
|
|
1012
|
+
args.push("--wait");
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
await runOpenStackCommand(args, 180000); // 3 minute timeout
|
|
1016
|
+
|
|
1017
|
+
operationHistory.push({
|
|
1018
|
+
type: "server_delete",
|
|
1019
|
+
server: params.server,
|
|
1020
|
+
timestamp: new Date().toISOString()
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
return {
|
|
1024
|
+
content: [{
|
|
1025
|
+
type: "text",
|
|
1026
|
+
text: JSON.stringify({
|
|
1027
|
+
success: true,
|
|
1028
|
+
message: `Server '${params.server}' deleted successfully`
|
|
1029
|
+
}, null, 2)
|
|
1030
|
+
}]
|
|
1031
|
+
};
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
return {
|
|
1034
|
+
content: [{
|
|
1035
|
+
type: "text",
|
|
1036
|
+
text: JSON.stringify({
|
|
1037
|
+
success: false,
|
|
1038
|
+
error: error.message
|
|
1039
|
+
}, null, 2)
|
|
1040
|
+
}]
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
// Start server
|
|
1046
|
+
server.registerTool("os_server_start", {
|
|
1047
|
+
title: "Start Server",
|
|
1048
|
+
description: "Start a stopped server",
|
|
1049
|
+
inputSchema: {
|
|
1050
|
+
type: "object",
|
|
1051
|
+
properties: {
|
|
1052
|
+
server: {
|
|
1053
|
+
type: "string",
|
|
1054
|
+
description: "Server ID or name to start"
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
required: ["server"],
|
|
1058
|
+
additionalProperties: false
|
|
1059
|
+
}
|
|
1060
|
+
}, async (params) => {
|
|
1061
|
+
try {
|
|
1062
|
+
await runOpenStackCommand(["server", "start", params.server]);
|
|
1063
|
+
|
|
1064
|
+
return {
|
|
1065
|
+
content: [{
|
|
1066
|
+
type: "text",
|
|
1067
|
+
text: JSON.stringify({
|
|
1068
|
+
success: true,
|
|
1069
|
+
message: `Server '${params.server}' start initiated`
|
|
1070
|
+
}, null, 2)
|
|
1071
|
+
}]
|
|
1072
|
+
};
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
return {
|
|
1075
|
+
content: [{
|
|
1076
|
+
type: "text",
|
|
1077
|
+
text: JSON.stringify({
|
|
1078
|
+
success: false,
|
|
1079
|
+
error: error.message
|
|
1080
|
+
}, null, 2)
|
|
1081
|
+
}]
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// Stop server
|
|
1087
|
+
server.registerTool("os_server_stop", {
|
|
1088
|
+
title: "Stop Server",
|
|
1089
|
+
description: "Stop a running server",
|
|
1090
|
+
inputSchema: {
|
|
1091
|
+
type: "object",
|
|
1092
|
+
properties: {
|
|
1093
|
+
server: {
|
|
1094
|
+
type: "string",
|
|
1095
|
+
description: "Server ID or name to stop"
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
required: ["server"],
|
|
1099
|
+
additionalProperties: false
|
|
1100
|
+
}
|
|
1101
|
+
}, async (params) => {
|
|
1102
|
+
try {
|
|
1103
|
+
await runOpenStackCommand(["server", "stop", params.server]);
|
|
1104
|
+
|
|
1105
|
+
return {
|
|
1106
|
+
content: [{
|
|
1107
|
+
type: "text",
|
|
1108
|
+
text: JSON.stringify({
|
|
1109
|
+
success: true,
|
|
1110
|
+
message: `Server '${params.server}' stop initiated`
|
|
1111
|
+
}, null, 2)
|
|
1112
|
+
}]
|
|
1113
|
+
};
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
return {
|
|
1116
|
+
content: [{
|
|
1117
|
+
type: "text",
|
|
1118
|
+
text: JSON.stringify({
|
|
1119
|
+
success: false,
|
|
1120
|
+
error: error.message
|
|
1121
|
+
}, null, 2)
|
|
1122
|
+
}]
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
// Reboot server
|
|
1128
|
+
server.registerTool("os_server_reboot", {
|
|
1129
|
+
title: "Reboot Server",
|
|
1130
|
+
description: "Reboot a server (soft or hard reboot)",
|
|
1131
|
+
inputSchema: {
|
|
1132
|
+
type: "object",
|
|
1133
|
+
properties: {
|
|
1134
|
+
server: {
|
|
1135
|
+
type: "string",
|
|
1136
|
+
description: "Server ID or name to reboot"
|
|
1137
|
+
},
|
|
1138
|
+
hard: {
|
|
1139
|
+
type: "boolean",
|
|
1140
|
+
description: "Perform hard reboot instead of soft reboot",
|
|
1141
|
+
default: false
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
required: ["server"],
|
|
1145
|
+
additionalProperties: false
|
|
1146
|
+
}
|
|
1147
|
+
}, async (params) => {
|
|
1148
|
+
try {
|
|
1149
|
+
const args = ["server", "reboot"];
|
|
1150
|
+
if (params.hard) {
|
|
1151
|
+
args.push("--hard");
|
|
1152
|
+
} else {
|
|
1153
|
+
args.push("--soft");
|
|
1154
|
+
}
|
|
1155
|
+
args.push(params.server);
|
|
1156
|
+
|
|
1157
|
+
await runOpenStackCommand(args);
|
|
1158
|
+
|
|
1159
|
+
return {
|
|
1160
|
+
content: [{
|
|
1161
|
+
type: "text",
|
|
1162
|
+
text: JSON.stringify({
|
|
1163
|
+
success: true,
|
|
1164
|
+
message: `Server '${params.server}' ${params.hard ? 'hard' : 'soft'} reboot initiated`
|
|
1165
|
+
}, null, 2)
|
|
1166
|
+
}]
|
|
1167
|
+
};
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
return {
|
|
1170
|
+
content: [{
|
|
1171
|
+
type: "text",
|
|
1172
|
+
text: JSON.stringify({
|
|
1173
|
+
success: false,
|
|
1174
|
+
error: error.message
|
|
1175
|
+
}, null, 2)
|
|
1176
|
+
}]
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
// Get server console URL
|
|
1182
|
+
server.registerTool("os_server_console", {
|
|
1183
|
+
title: "Get Server Console",
|
|
1184
|
+
description: "Get console URL for accessing server",
|
|
1185
|
+
inputSchema: {
|
|
1186
|
+
type: "object",
|
|
1187
|
+
properties: {
|
|
1188
|
+
server: {
|
|
1189
|
+
type: "string",
|
|
1190
|
+
description: "Server ID or name"
|
|
1191
|
+
},
|
|
1192
|
+
type: {
|
|
1193
|
+
type: "string",
|
|
1194
|
+
enum: ["novnc", "xvpvnc", "spice", "rdp", "serial"],
|
|
1195
|
+
description: "Console type",
|
|
1196
|
+
default: "novnc"
|
|
1197
|
+
}
|
|
1198
|
+
},
|
|
1199
|
+
required: ["server"],
|
|
1200
|
+
additionalProperties: false
|
|
1201
|
+
}
|
|
1202
|
+
}, async (params) => {
|
|
1203
|
+
try {
|
|
1204
|
+
const result = await runOpenStackCommand([
|
|
1205
|
+
"console", "url", "show",
|
|
1206
|
+
"--" + (params.type || "novnc"),
|
|
1207
|
+
params.server,
|
|
1208
|
+
"-f", "json"
|
|
1209
|
+
]);
|
|
1210
|
+
const console_info = JSON.parse(result.output);
|
|
1211
|
+
|
|
1212
|
+
return {
|
|
1213
|
+
content: [{
|
|
1214
|
+
type: "text",
|
|
1215
|
+
text: JSON.stringify({
|
|
1216
|
+
success: true,
|
|
1217
|
+
console: {
|
|
1218
|
+
type: console_info.type,
|
|
1219
|
+
url: console_info.url
|
|
1220
|
+
}
|
|
1221
|
+
}, null, 2)
|
|
1222
|
+
}]
|
|
1223
|
+
};
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
return {
|
|
1226
|
+
content: [{
|
|
1227
|
+
type: "text",
|
|
1228
|
+
text: JSON.stringify({
|
|
1229
|
+
success: false,
|
|
1230
|
+
error: error.message
|
|
1231
|
+
}, null, 2)
|
|
1232
|
+
}]
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
// Attach floating IP
|
|
1238
|
+
server.registerTool("os_floating_ip_create", {
|
|
1239
|
+
title: "Create Floating IP",
|
|
1240
|
+
description: "Create a new floating IP from an external network",
|
|
1241
|
+
inputSchema: {
|
|
1242
|
+
type: "object",
|
|
1243
|
+
properties: {
|
|
1244
|
+
network: {
|
|
1245
|
+
type: "string",
|
|
1246
|
+
description: "External network name or ID"
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
required: ["network"],
|
|
1250
|
+
additionalProperties: false
|
|
1251
|
+
}
|
|
1252
|
+
}, async (params) => {
|
|
1253
|
+
try {
|
|
1254
|
+
const result = await runOpenStackCommand([
|
|
1255
|
+
"floating", "ip", "create",
|
|
1256
|
+
params.network,
|
|
1257
|
+
"-f", "json"
|
|
1258
|
+
]);
|
|
1259
|
+
const floating_ip = JSON.parse(result.output);
|
|
1260
|
+
|
|
1261
|
+
return {
|
|
1262
|
+
content: [{
|
|
1263
|
+
type: "text",
|
|
1264
|
+
text: JSON.stringify({
|
|
1265
|
+
success: true,
|
|
1266
|
+
floating_ip: {
|
|
1267
|
+
id: floating_ip.id,
|
|
1268
|
+
ip: floating_ip.floating_ip_address,
|
|
1269
|
+
network: floating_ip.floating_network_id,
|
|
1270
|
+
status: floating_ip.status
|
|
1271
|
+
},
|
|
1272
|
+
message: "Floating IP created successfully"
|
|
1273
|
+
}, null, 2)
|
|
1274
|
+
}]
|
|
1275
|
+
};
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
return {
|
|
1278
|
+
content: [{
|
|
1279
|
+
type: "text",
|
|
1280
|
+
text: JSON.stringify({
|
|
1281
|
+
success: false,
|
|
1282
|
+
error: error.message
|
|
1283
|
+
}, null, 2)
|
|
1284
|
+
}]
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
// List floating IPs
|
|
1290
|
+
server.registerTool("os_floating_ip_list", {
|
|
1291
|
+
title: "List Floating IPs",
|
|
1292
|
+
description: "List all floating IPs in the project",
|
|
1293
|
+
inputSchema: {
|
|
1294
|
+
type: "object",
|
|
1295
|
+
properties: {},
|
|
1296
|
+
additionalProperties: false
|
|
1297
|
+
}
|
|
1298
|
+
}, async () => {
|
|
1299
|
+
try {
|
|
1300
|
+
const result = await runOpenStackCommand(["floating", "ip", "list", "-f", "json"]);
|
|
1301
|
+
const floating_ips = JSON.parse(result.output);
|
|
1302
|
+
|
|
1303
|
+
return {
|
|
1304
|
+
content: [{
|
|
1305
|
+
type: "text",
|
|
1306
|
+
text: JSON.stringify({
|
|
1307
|
+
success: true,
|
|
1308
|
+
count: floating_ips.length,
|
|
1309
|
+
floating_ips: floating_ips.map(f => ({
|
|
1310
|
+
id: f.ID,
|
|
1311
|
+
ip: f["Floating IP Address"],
|
|
1312
|
+
fixed_ip: f["Fixed IP Address"],
|
|
1313
|
+
port: f.Port,
|
|
1314
|
+
status: f.Status
|
|
1315
|
+
}))
|
|
1316
|
+
}, null, 2)
|
|
1317
|
+
}]
|
|
1318
|
+
};
|
|
1319
|
+
} catch (error) {
|
|
1320
|
+
return {
|
|
1321
|
+
content: [{
|
|
1322
|
+
type: "text",
|
|
1323
|
+
text: JSON.stringify({
|
|
1324
|
+
success: false,
|
|
1325
|
+
error: error.message
|
|
1326
|
+
}, null, 2)
|
|
1327
|
+
}]
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
// Associate floating IP with server
|
|
1333
|
+
server.registerTool("os_server_add_floating_ip", {
|
|
1334
|
+
title: "Add Floating IP to Server",
|
|
1335
|
+
description: "Associate a floating IP with a server",
|
|
1336
|
+
inputSchema: {
|
|
1337
|
+
type: "object",
|
|
1338
|
+
properties: {
|
|
1339
|
+
server: {
|
|
1340
|
+
type: "string",
|
|
1341
|
+
description: "Server ID or name"
|
|
1342
|
+
},
|
|
1343
|
+
floating_ip: {
|
|
1344
|
+
type: "string",
|
|
1345
|
+
description: "Floating IP address to associate"
|
|
1346
|
+
}
|
|
1347
|
+
},
|
|
1348
|
+
required: ["server", "floating_ip"],
|
|
1349
|
+
additionalProperties: false
|
|
1350
|
+
}
|
|
1351
|
+
}, async (params) => {
|
|
1352
|
+
try {
|
|
1353
|
+
await runOpenStackCommand([
|
|
1354
|
+
"server", "add", "floating", "ip",
|
|
1355
|
+
params.server,
|
|
1356
|
+
params.floating_ip
|
|
1357
|
+
]);
|
|
1358
|
+
|
|
1359
|
+
return {
|
|
1360
|
+
content: [{
|
|
1361
|
+
type: "text",
|
|
1362
|
+
text: JSON.stringify({
|
|
1363
|
+
success: true,
|
|
1364
|
+
message: `Floating IP '${params.floating_ip}' associated with server '${params.server}'`
|
|
1365
|
+
}, null, 2)
|
|
1366
|
+
}]
|
|
1367
|
+
};
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
return {
|
|
1370
|
+
content: [{
|
|
1371
|
+
type: "text",
|
|
1372
|
+
text: JSON.stringify({
|
|
1373
|
+
success: false,
|
|
1374
|
+
error: error.message
|
|
1375
|
+
}, null, 2)
|
|
1376
|
+
}]
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
// Volume operations
|
|
1382
|
+
server.registerTool("os_volume_list", {
|
|
1383
|
+
title: "List Volumes",
|
|
1384
|
+
description: "List all volumes in the project",
|
|
1385
|
+
inputSchema: {
|
|
1386
|
+
type: "object",
|
|
1387
|
+
properties: {},
|
|
1388
|
+
additionalProperties: false
|
|
1389
|
+
}
|
|
1390
|
+
}, async () => {
|
|
1391
|
+
try {
|
|
1392
|
+
const result = await runOpenStackCommand(["volume", "list", "-f", "json"]);
|
|
1393
|
+
const volumes = JSON.parse(result.output);
|
|
1394
|
+
|
|
1395
|
+
return {
|
|
1396
|
+
content: [{
|
|
1397
|
+
type: "text",
|
|
1398
|
+
text: JSON.stringify({
|
|
1399
|
+
success: true,
|
|
1400
|
+
count: volumes.length,
|
|
1401
|
+
volumes: volumes.map(v => ({
|
|
1402
|
+
id: v.ID,
|
|
1403
|
+
name: v.Name,
|
|
1404
|
+
status: v.Status,
|
|
1405
|
+
size_gb: v.Size,
|
|
1406
|
+
attached_to: v["Attached to"]
|
|
1407
|
+
}))
|
|
1408
|
+
}, null, 2)
|
|
1409
|
+
}]
|
|
1410
|
+
};
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
return {
|
|
1413
|
+
content: [{
|
|
1414
|
+
type: "text",
|
|
1415
|
+
text: JSON.stringify({
|
|
1416
|
+
success: false,
|
|
1417
|
+
error: error.message
|
|
1418
|
+
}, null, 2)
|
|
1419
|
+
}]
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
// Create volume
|
|
1425
|
+
server.registerTool("os_volume_create", {
|
|
1426
|
+
title: "Create Volume",
|
|
1427
|
+
description: "Create a new block storage volume",
|
|
1428
|
+
inputSchema: {
|
|
1429
|
+
type: "object",
|
|
1430
|
+
properties: {
|
|
1431
|
+
name: {
|
|
1432
|
+
type: "string",
|
|
1433
|
+
description: "Volume name"
|
|
1434
|
+
},
|
|
1435
|
+
size: {
|
|
1436
|
+
type: "number",
|
|
1437
|
+
description: "Volume size in GB"
|
|
1438
|
+
},
|
|
1439
|
+
type: {
|
|
1440
|
+
type: "string",
|
|
1441
|
+
description: "Volume type (optional)"
|
|
1442
|
+
},
|
|
1443
|
+
image: {
|
|
1444
|
+
type: "string",
|
|
1445
|
+
description: "Image ID to create volume from (optional)"
|
|
1446
|
+
}
|
|
1447
|
+
},
|
|
1448
|
+
required: ["name", "size"],
|
|
1449
|
+
additionalProperties: false
|
|
1450
|
+
}
|
|
1451
|
+
}, async (params) => {
|
|
1452
|
+
try {
|
|
1453
|
+
const args = ["volume", "create"];
|
|
1454
|
+
args.push("--size", params.size.toString());
|
|
1455
|
+
|
|
1456
|
+
if (params.type) {
|
|
1457
|
+
args.push("--type", params.type);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (params.image) {
|
|
1461
|
+
args.push("--image", params.image);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
args.push("-f", "json");
|
|
1465
|
+
args.push(params.name);
|
|
1466
|
+
|
|
1467
|
+
const result = await runOpenStackCommand(args, 180000);
|
|
1468
|
+
const volume = JSON.parse(result.output);
|
|
1469
|
+
|
|
1470
|
+
return {
|
|
1471
|
+
content: [{
|
|
1472
|
+
type: "text",
|
|
1473
|
+
text: JSON.stringify({
|
|
1474
|
+
success: true,
|
|
1475
|
+
volume: {
|
|
1476
|
+
id: volume.id,
|
|
1477
|
+
name: volume.name,
|
|
1478
|
+
size: volume.size,
|
|
1479
|
+
status: volume.status,
|
|
1480
|
+
type: volume.type
|
|
1481
|
+
},
|
|
1482
|
+
message: "Volume created successfully"
|
|
1483
|
+
}, null, 2)
|
|
1484
|
+
}]
|
|
1485
|
+
};
|
|
1486
|
+
} catch (error) {
|
|
1487
|
+
return {
|
|
1488
|
+
content: [{
|
|
1489
|
+
type: "text",
|
|
1490
|
+
text: JSON.stringify({
|
|
1491
|
+
success: false,
|
|
1492
|
+
error: error.message
|
|
1493
|
+
}, null, 2)
|
|
1494
|
+
}]
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
// Attach volume to server
|
|
1500
|
+
server.registerTool("os_server_add_volume", {
|
|
1501
|
+
title: "Attach Volume to Server",
|
|
1502
|
+
description: "Attach a volume to a server",
|
|
1503
|
+
inputSchema: {
|
|
1504
|
+
type: "object",
|
|
1505
|
+
properties: {
|
|
1506
|
+
server: {
|
|
1507
|
+
type: "string",
|
|
1508
|
+
description: "Server ID or name"
|
|
1509
|
+
},
|
|
1510
|
+
volume: {
|
|
1511
|
+
type: "string",
|
|
1512
|
+
description: "Volume ID or name"
|
|
1513
|
+
},
|
|
1514
|
+
device: {
|
|
1515
|
+
type: "string",
|
|
1516
|
+
description: "Device path (e.g., /dev/vdb), auto-assigned if not specified"
|
|
1517
|
+
}
|
|
1518
|
+
},
|
|
1519
|
+
required: ["server", "volume"],
|
|
1520
|
+
additionalProperties: false
|
|
1521
|
+
}
|
|
1522
|
+
}, async (params) => {
|
|
1523
|
+
try {
|
|
1524
|
+
const args = ["server", "add", "volume"];
|
|
1525
|
+
|
|
1526
|
+
if (params.device) {
|
|
1527
|
+
args.push("--device", params.device);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
args.push(params.server);
|
|
1531
|
+
args.push(params.volume);
|
|
1532
|
+
|
|
1533
|
+
await runOpenStackCommand(args);
|
|
1534
|
+
|
|
1535
|
+
return {
|
|
1536
|
+
content: [{
|
|
1537
|
+
type: "text",
|
|
1538
|
+
text: JSON.stringify({
|
|
1539
|
+
success: true,
|
|
1540
|
+
message: `Volume '${params.volume}' attached to server '${params.server}'`
|
|
1541
|
+
}, null, 2)
|
|
1542
|
+
}]
|
|
1543
|
+
};
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
return {
|
|
1546
|
+
content: [{
|
|
1547
|
+
type: "text",
|
|
1548
|
+
text: JSON.stringify({
|
|
1549
|
+
success: false,
|
|
1550
|
+
error: error.message
|
|
1551
|
+
}, null, 2)
|
|
1552
|
+
}]
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
// Get operation history
|
|
1558
|
+
server.registerTool("os_operation_history", {
|
|
1559
|
+
title: "Get Operation History",
|
|
1560
|
+
description: "Get history of OpenStack operations performed",
|
|
1561
|
+
inputSchema: {
|
|
1562
|
+
type: "object",
|
|
1563
|
+
properties: {
|
|
1564
|
+
limit: {
|
|
1565
|
+
type: "number",
|
|
1566
|
+
description: "Maximum number of operations to return",
|
|
1567
|
+
default: 20
|
|
1568
|
+
}
|
|
1569
|
+
},
|
|
1570
|
+
additionalProperties: false
|
|
1571
|
+
}
|
|
1572
|
+
}, async (params) => {
|
|
1573
|
+
const limit = params.limit || 20;
|
|
1574
|
+
const history = operationHistory.slice(-limit);
|
|
1575
|
+
|
|
1576
|
+
return {
|
|
1577
|
+
content: [{
|
|
1578
|
+
type: "text",
|
|
1579
|
+
text: JSON.stringify({
|
|
1580
|
+
success: true,
|
|
1581
|
+
count: history.length,
|
|
1582
|
+
operations: history
|
|
1583
|
+
}, null, 2)
|
|
1584
|
+
}]
|
|
1585
|
+
};
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
// Start the MCP server
|
|
1589
|
+
async function main() {
|
|
1590
|
+
try {
|
|
1591
|
+
const transport = new StdioServerTransport();
|
|
1592
|
+
await server.connect(transport);
|
|
1593
|
+
|
|
1594
|
+
console.error("OpenStack MCP Server v1.0.0 running...");
|
|
1595
|
+
console.error(`Auth URL: ${OS_AUTH_URL || "Not configured"}`);
|
|
1596
|
+
console.error(`Region: ${OS_REGION_NAME || "Not configured"}`);
|
|
1597
|
+
|
|
1598
|
+
} catch (error) {
|
|
1599
|
+
console.error("Failed to start server:", error.message);
|
|
1600
|
+
process.exit(1);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
main().catch((error) => {
|
|
1605
|
+
console.error("Server startup error:", error);
|
|
1606
|
+
process.exit(1);
|
|
1607
|
+
});
|