myaidev-method 0.2.18 → 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.
Files changed (31) hide show
  1. package/.claude/mcp/sparc-orchestrator-server.js +0 -0
  2. package/.claude/mcp/wordpress-server.js +0 -0
  3. package/CHANGELOG.md +145 -0
  4. package/README.md +205 -13
  5. package/TECHNICAL_ARCHITECTURE.md +64 -2
  6. package/bin/cli.js +169 -2
  7. package/dist/mcp/mcp-config.json +138 -1
  8. package/dist/mcp/openstack-server.js +1607 -0
  9. package/package.json +2 -2
  10. package/src/config/workflows.js +532 -0
  11. package/src/lib/payloadcms-utils.js +343 -10
  12. package/src/lib/visual-generation-utils.js +445 -294
  13. package/src/lib/workflow-installer.js +512 -0
  14. package/src/libs/security/authorization-checker.js +606 -0
  15. package/src/mcp/openstack-server.js +1607 -0
  16. package/src/scripts/openstack-setup.sh +110 -0
  17. package/src/scripts/security/environment-detect.js +425 -0
  18. package/src/templates/claude/agents/openstack-vm-manager.md +281 -0
  19. package/src/templates/claude/agents/osint-researcher.md +1075 -0
  20. package/src/templates/claude/agents/penetration-tester.md +908 -0
  21. package/src/templates/claude/agents/security-auditor.md +244 -0
  22. package/src/templates/claude/agents/security-setup.md +1094 -0
  23. package/src/templates/claude/agents/webapp-security-tester.md +581 -0
  24. package/src/templates/claude/commands/myai-configure.md +84 -0
  25. package/src/templates/claude/commands/myai-openstack.md +229 -0
  26. package/src/templates/claude/commands/sc:security-exploit.md +464 -0
  27. package/src/templates/claude/commands/sc:security-recon.md +281 -0
  28. package/src/templates/claude/commands/sc:security-report.md +756 -0
  29. package/src/templates/claude/commands/sc:security-scan.md +441 -0
  30. package/src/templates/claude/commands/sc:security-setup.md +501 -0
  31. 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
+ });