ludus-mcp
Version:
MCP server for managing Ludus cybersecurity training environments through natural language commands
1,307 lines (1,195 loc) • 84.7 kB
text/typescript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListPromptsRequestSchema,
GetPromptRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { Logger } from './utils/logger.js';
import { InteractiveSetup, LudusConfig } from './ludusMCP/interactiveSetup.js';
import path from 'path';
import { LudusCliWrapper } from './ludusMCP/cliWrapper.js';
import { ALL_PROMPTS } from './prompts/index.js';
import { handleCreateLudusRangePrompt } from './prompts/createLudusRange.js';
import { handleExecuteLudusCmdPrompt } from './prompts/executeLudusCmd.js';
import {
deployRangeTool,
getRangeStatusTool,
listUserRangesTool,
getConnectionInfoTool,
destroyRangeTool,
rangeAbortTool,
getTagsTool,
ludusCliExecuteTool,
ludusHelpTool,
listAllUsersTool,
readRangeConfigTool,
writeRangeConfigTool,
validateRangeConfigTool,
listRangeConfigsTool,
getRangeConfigTool,
setRangeConfigTool,
ludusPowerTool,
getCredentialFromUserTool,
insertCredsRangeConfigTool,
ludusDocsSearchTool,
ludusRangePlannerTool,
ludusRolesSearchTool,
ludusEnvironmentGuidesSearchTool,
ludusNetworkingSearchTool,
ludusReadRangeConfigSchemaTool,
ludusRangeConfigCheckAgainstPlanTool,
ludusReadRoleCollectionSchemaTool,
ludusListRoleCollectionSchemasTool
} from './tools/index.js';
import { handleListAllUsers } from './tools/listAllUsers.js';
import { handleReadRangeConfig, handleWriteRangeConfig, handleValidateRangeConfig, handleListRangeConfigs, handleGetRangeConfig } from './tools/rangeConfig.js';
import { handleInsertCredsRangeConfig } from './tools/insertCredsRangeConfig.js';
import { handleSetRangeConfig } from './tools/setRangeConfig.js';
import { handleGetConnectionInfo } from './tools/getConnectionInfo.js';
import { handleDestroyRange } from './tools/destroyRange.js';
import { handleLudusPower } from './tools/ludusPower.js';
import { handleGetCredentialFromUser } from './tools/getCredentialFromUser.js';
import { handleLudusDocsSearch } from './tools/ludusDocsSearch.js';
import { handleLudusRangePlanner } from './tools/ludusRangePlanner.js';
import { handleLudusRolesDocsRead } from './tools/ludusRolesSearch.js';
import { handleLudusEnvironmentGuidesSearch } from './tools/ludusEnvironmentGuidesSearch.js';
import { handleLudusNetworkingDocsRead } from './tools/ludusNetworkingSearch.js';
import { handleLudusReadRangeConfigSchema } from './tools/ludusReadRangeConfigSchema.js';
import { handleLudusRangeConfigCheckAgainstPlan } from './tools/ludusRangeConfigCheckAgainstPlan.js';
import { handleLudusReadRoleCollectionSchema } from './tools/ludusReadRoleCollectionSchema.js';
import { handleLudusListRoleCollectionSchemas } from './tools/ludusListRoleCollectionSchemas.js';
import {
getCredentials,
CREDENTIAL_KEYS,
isKeyringSupportAvailable
} from './utils/keyring.js';
import { downloadLudusDocumentation } from './utils/downloadDocs.js';
import { downloadBaseConfigs } from './utils/downloadBaseConfigs.js';
import { downloadSchemas } from './utils/downloadSchemas.js';
class LudusMCPServer {
private server: Server;
private logger: Logger;
private ludusConfig: LudusConfig | null = null;
private ludusCliWrapper: LudusCliWrapper | null = null;
constructor() {
this.logger = new Logger('LudusMCPServer');
this.logger.info('Ludus MCP Server starting', {
nodeVersion: process.version,
platform: process.platform,
workingDir: process.cwd(),
args: process.argv,
env: {
NODE_ENV: process.env.NODE_ENV,
LUDUS_DEBUG: process.env.LUDUS_DEBUG,
LUDUS_FILE_LOGGING: process.env.LUDUS_FILE_LOGGING,
PATH: process.env.PATH?.substring(0, 200) + '...'
}
});
this.server = new Server(
{
name: 'ludus-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
prompts: {},
},
}
);
this.setupToolHandlers();
this.setupPromptHandlers();
this.setupErrorHandlers();
}
private setupToolHandlers(): void {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
deployRangeTool,
getRangeStatusTool,
listUserRangesTool,
getConnectionInfoTool,
destroyRangeTool,
rangeAbortTool,
getTagsTool,
ludusCliExecuteTool,
ludusHelpTool,
listAllUsersTool,
readRangeConfigTool,
writeRangeConfigTool,
validateRangeConfigTool,
listRangeConfigsTool,
getRangeConfigTool,
setRangeConfigTool,
ludusPowerTool,
getCredentialFromUserTool,
insertCredsRangeConfigTool,
ludusDocsSearchTool,
ludusRangePlannerTool,
ludusRolesSearchTool,
ludusEnvironmentGuidesSearchTool,
ludusNetworkingSearchTool,
ludusReadRangeConfigSchemaTool,
ludusRangeConfigCheckAgainstPlanTool,
ludusReadRoleCollectionSchemaTool,
ludusListRoleCollectionSchemasTool
],
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'deploy_range':
return await this.handleDeployRange(args);
case 'get_range_status':
return await this.handleGetRangeStatus(args);
case 'list_user_ranges':
return await this.handleListUserRanges(args);
case 'get_connection_info':
return await this.handleGetConnectionInfo(args);
case 'destroy_range':
return await this.handleDestroyRange(args);
case 'range_abort':
return await this.handleRangeAbort(args);
case 'get_tags':
return await this.handleGetTags(args);
case 'ludus_cli_execute':
return await this.handleLudusCliExecute(args);
case 'ludus_help':
return await this.handleLudusHelp(args);
case 'list_all_users':
return await this.handleListAllUsers(args);
case 'read_range_config':
return await this.handleReadRangeConfig(args);
case 'write_range_config':
return await this.handleWriteRangeConfig(args);
case 'validate_range_config':
return await this.handleValidateRangeConfig(args);
case 'list_range_configs':
return await this.handleListRangeConfigs(args);
case 'get_range_config':
return await this.handleGetRangeConfig(args);
case 'set_range_config':
return await this.handleSetRangeConfig(args);
case 'ludus_power':
return await this.handleLudusPower(args);
case 'get_credential_from_user':
return await this.handleGetCredentialFromUser(args);
case 'insert_creds_range_config':
return await this.handleInsertCredsRangeConfig(args);
case 'ludus_docs_search':
return await this.handleLudusDocsSearch(args);
case 'ludus_range_planner':
return await this.handleLudusRangePlanner(args);
case 'ludus_roles_search':
return await this.handleLudusRolesDocsRead(args);
case 'ludus_environment_guides_search':
return await this.handleLudusEnvironmentGuidesSearch(args);
case 'ludus_networking_search':
return await this.handleLudusNetworkingDocsRead(args);
case 'ludus_read_range_config_schema':
return await this.handleLudusReadRangeConfigSchema(args);
case 'ludus_range_config_check_against_plan':
return await this.handleLudusRangeConfigCheckAgainstPlan(args);
case 'ludus_read_role_collection_schema':
return await this.handleLudusReadRoleCollectionSchema(args);
case 'ludus_list_role_collection_schemas':
return await this.handleLudusListRoleCollectionSchemas(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
this.logger.error('Tool execution failed', error, { tool: name });
throw error;
}
});
}
private async handleDeployRange(args: any) {
const {
user,
configPath,
force = false,
tags,
limit,
onlyRoles,
verboseAnsible = false
} = args;
this.logger.info('Deploying range', {
user,
configPath,
force,
tags,
limit,
onlyRoles,
verboseAnsible
});
if (!this.ludusCliWrapper) {
return {
content: [
{
type: 'text',
text: `Server not fully initialized yet. Please wait a moment and try again.\n\n` +
`The server is still setting up connections and validating credentials.\n` +
`This usually takes 10-30 seconds after startup.`
}
]
};
}
try {
// Build options object, filtering out undefined values
const deployOptions: any = { force, verboseAnsible };
if (user !== undefined) deployOptions.user = user;
if (configPath !== undefined) deployOptions.configPath = configPath;
if (tags !== undefined) deployOptions.tags = tags;
if (limit !== undefined) deployOptions.limit = limit;
if (onlyRoles !== undefined) deployOptions.onlyRoles = onlyRoles;
// Deploy the range with all options
const result = await this.ludusCliWrapper!.deployRange(deployOptions);
if (!result.success) {
throw new Error(`Failed to deploy range: ${result.message}`);
}
const targetUser = user || 'current user';
return {
content: [
{
type: 'text',
text: `Range deployment initiated successfully for ${targetUser}\n\n` +
`${configPath ? `Configuration: ${configPath}\n` : 'Using existing configuration\n'}` +
`Status: ${result.data ? JSON.stringify(result.data, null, 2) : 'Deployment started'}\n\n` +
`Deployment Progress:\n` +
`- Configuration applied\n` +
`- VMs are being created and configured\n` +
`- This typically takes 10-30 minutes\n\n` +
`Next Steps:\n` +
`- Monitor progress: get_range_status()\n` +
`- Get connection info once ready: get_connection_info()\n` +
`- Check deployment logs if needed`
}
]
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Range deployment failed: ${error.message}\n\n` +
`Troubleshooting:\n` +
`- Verify configuration file is valid (if provided)\n` +
`- Check if you have permission to deploy ranges\n` +
`- Ensure WireGuard VPN is connected\n` +
`- Try: list_user_ranges() to check current status`
}
]
};
}
}
private async handleGetRangeStatus(args: any) {
const { user } = args;
this.logger.info('Getting range status', { user });
try {
const result = await this.ludusCliWrapper!.getRangeStatus(user);
if (!result.success) {
throw new Error(`Failed to get range status: ${result.message}`);
}
const targetUser = user || 'current user';
return {
content: [
{
type: 'text',
text: `Range Status for ${targetUser}\n\n` +
`${JSON.stringify(result.data, null, 2)}\n\n` +
`Usage:\n` +
`- If deploying: Wait for completion, then use get_connection_info()\n` +
`- If ready: Use get_connection_info() to access VMs\n` +
`- If stopped: Use deploy_range() to deploy a new range`
}
]
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Failed to get range status: ${error.message}\n\n` +
`Troubleshooting:\n` +
`- Verify the user has a deployed range\n` +
`- Check admin permissions (if querying other users)\n` +
`- Try: list_user_ranges() to see all ranges\n` +
`- Deploy a range first if none exists`
}
]
};
}
}
private async handleListUserRanges(args: any) {
const { user } = args;
this.logger.info('Listing user ranges', { user });
try {
const result = await this.ludusCliWrapper!.listUserRanges(user);
if (!result.success) {
throw new Error(`Failed to list ranges: ${result.message}`);
}
const targetUser = user || 'current user';
const ranges = result.data;
let statusText = `Ranges for ${targetUser}\n\n`;
if (Array.isArray(ranges) && ranges.length > 0) {
statusText += `Found ${ranges.length} range(s):\n\n`;
statusText += JSON.stringify(ranges, null, 2);
} else if (ranges) {
statusText += `Range information:\n\n`;
statusText += JSON.stringify(ranges, null, 2);
} else {
statusText += `No ranges found.\n\n`;
statusText += `Next Steps:\n`;
statusText += `- Deploy your first range: deploy_range()\n`;
statusText += `- Check available templates: list_templates()`;
}
return {
content: [
{
type: 'text',
text: statusText
}
]
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Failed to list ranges: ${error.message}\n\n` +
`Troubleshooting:\n` +
`- Verify the user exists in Ludus\n` +
`- Check admin permissions (if querying other users)\n` +
`- Ensure WireGuard VPN is connected\n` +
`- Try deploying a range first if none exist`
}
]
};
}
}
private async handleGetConnectionInfo(args: any) {
this.logger.info('Getting connection info via tool wrapper', { args });
try {
// Use the tool wrapper that includes download location information
const result = await handleGetConnectionInfo(args, this.logger, this.ludusCliWrapper!);
if (!result.success) {
throw new Error(result.message);
}
const targetUser = result.user;
const downloadLocation = result.rdpDownloadLocation;
let connectionText = `🔗 Connection Information for ${targetUser}\n`;
connectionText += `📂 Files saved to: ${downloadLocation}\n\n`;
if (result.connections.rdp?.available) {
connectionText += `RDP Configuration:\n`;
connectionText += ` File: ${result.connections.rdp.filePath}\n`;
connectionText += ` ${result.connections.rdp.usage}\n\n`;
}
if (result.connections.wireguard?.available) {
connectionText += `WireGuard VPN Configuration:\n`;
connectionText += ` File: ${result.connections.wireguard.filePath}\n`;
connectionText += ` ${result.connections.wireguard.usage}\n\n`;
}
if (result.connections.etcHosts?.available) {
connectionText += `/etc/hosts Entries:\n`;
connectionText += ` File: ${result.connections.etcHosts.filePath}\n`;
connectionText += ` ${result.connections.etcHosts.usage}\n\n`;
}
connectionText += `Usage Instructions:\n`;
if (result.usageInstructions && result.usageInstructions.length > 0) {
result.usageInstructions.forEach((instruction: string) => {
connectionText += `- ${instruction}\n`;
});
}
return {
content: [
{
type: 'text',
text: connectionText
}
]
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Failed to get connection info: ${error.message}\n\n` +
`Troubleshooting:\n` +
`- Verify the user has a deployed range\n` +
`- Check if range is fully deployed (not just deploying)\n` +
`- Try: get_range_status() to check deployment progress\n` +
`- Ensure admin permissions (if querying other users)`
}
]
};
}
}
private async handleDestroyRange(args: any) {
const { user, force = false } = args;
this.logger.info('Destroying range', { user, force });
try {
const targetUser = user || 'current user';
// Get current status first
const statusResult = await this.ludusCliWrapper!.getRangeStatus(user);
let currentStatus = 'unknown';
if (statusResult.success && statusResult.data) {
currentStatus = typeof statusResult.data === 'object' ?
(statusResult.data.status || 'active') : 'active';
}
// Destroy the range
const result = await this.ludusCliWrapper!.destroyRange(user, force);
if (!result.success) {
throw new Error(`Failed to stop range: ${result.message}`);
}
return {
content: [
{
type: 'text',
text: `Range successfully stopped for ${targetUser}\n\n` +
`Previous Status: ${currentStatus}\n` +
`Result: ${JSON.stringify(result.data, null, 2)}\n\n` +
`Important: Range has been permanently destroyed\n` +
`- All VMs and data have been removed\n` +
`- Resources have been freed for new deployments\n\n` +
`Next Steps:\n` +
`- Deploy a new range: deploy_range()\n` +
`- Check available templates if needed\n` +
`- All previous data is permanently deleted`
}
]
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Failed to stop range: ${error.message}\n\n` +
`Troubleshooting:\n` +
`- Verify the user has an active range to stop\n` +
`- Check admin permissions (if stopping other users' ranges)\n` +
`- Try: get_range_status() to check current status\n` +
`- Use force=true if there are confirmation prompts`
}
]
};
}
}
private async handleRangeAbort(args: any) {
const { user } = args;
this.logger.info('Aborting range deployment', { user });
if (!this.ludusCliWrapper) {
return {
content: [
{
type: 'text',
text: `Server not fully initialized yet. Please wait a moment and try again.\n\n` +
`The server is still setting up connections and validating credentials.\n` +
`This usually takes 10-30 seconds after startup.`
}
]
};
}
try {
const result = await this.ludusCliWrapper.abortRange(user);
if (!result.success) {
throw new Error(`Failed to abort range deployment: ${result.message}`);
}
const targetUser = user || 'current user';
return {
content: [
{
type: 'text',
text: `Range deployment aborted successfully for ${targetUser}\n\n` +
`Deployment Process Killed:\n` +
`- Ansible deployment process terminated\n` +
`- All pending deployment tasks stopped\n` +
`- VMs may be in partial deployment state\n\n` +
`Next Steps:\n` +
`- Check range status: get_range_status()\n` +
`- Deploy again if needed: deploy_range()\n` +
`- View logs for issues: range_logs()`
}
]
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Failed to abort range deployment: ${error.message}\n\n` +
`Troubleshooting:\n` +
`- Verify there is an active deployment to abort\n` +
`- Check admin permissions (if aborting other users' deployments)\n` +
`- Try: get_range_status() to check current status\n` +
`- Check deployment logs for more details`
}
]
};
}
}
private async handleGetTags(args: any) {
const { user } = args;
this.logger.info('Getting available deployment tags', { user });
if (!this.ludusCliWrapper) {
return {
content: [
{
type: 'text',
text: `Server not fully initialized yet. Please wait a moment and try again.\n\n` +
`The server is still setting up connections and validating credentials.\n` +
`This usually takes 10-30 seconds after startup.`
}
]
};
}
try {
const result = await this.ludusCliWrapper.getTags(user);
if (!result.success) {
throw new Error(`Failed to get deployment tags: ${result.message}`);
}
const targetUser = user || 'current user';
return {
content: [
{
type: 'text',
text: `Available Deployment Tags for ${targetUser}\n\n` +
`${JSON.stringify(result.data, null, 2)}\n\n` +
`Usage with deploy_range:\n` +
`- Use single tag: deploy_range({ tags: "dns" })\n` +
`- Use multiple tags: deploy_range({ tags: "dns,custom-groups" })\n` +
`- Example: deploy_range({ tags: "dns,custom-groups,baseline" })\n\n` +
`Common Tags:\n` +
`- baseline: Basic system configuration\n` +
`- dns: DNS server setup\n` +
`- custom-groups: User/group configuration\n` +
`- testing: Enable testing mode features`
}
]
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Failed to get deployment tags: ${error.message}\n\n` +
`Troubleshooting:\n` +
`- Verify you have a configured range\n` +
`- Check admin permissions (if getting tags for other users)\n` +
`- Try: get_range_status() to check range configuration\n` +
`- Ensure range configuration is valid`
}
]
};
}
}
private async handleLudusCliExecute(args: any) {
const { command, args: cmdArgs = [], user } = args;
const fullCommand = `ludus ${command} ${cmdArgs.join(' ')}`.trim();
if (!this.ludusCliWrapper) {
return {
content: [
{
type: 'text',
text: `Server not fully initialized yet. Please wait a moment and try again.\n\n` +
`The server is still setting up connections and validating credentials.\n` +
`This usually takes 10-30 seconds after startup.`
}
]
};
}
try {
// Parse command and arguments
let parsedCommand = command;
let parsedArgs = [...cmdArgs];
// If command contains spaces, split it
if (command.includes(' ')) {
const parts = command.split(' ');
parsedCommand = parts[0];
parsedArgs = [...parts.slice(1), ...cmdArgs];
}
// Add user flag if provided
if (user) {
parsedArgs.push('--user', user);
}
// Execute the command
const result = await this.ludusCliWrapper.executeArbitraryCommand(parsedCommand, parsedArgs);
// Format the response
const targetUser = user || 'current user';
const status = result.success ? '' : '';
return {
content: [
{
type: 'text',
text: `${status} Ludus CLI Command: \`${fullCommand}\`\n` +
`👤 User: ${targetUser}\n` +
`Status: ${result.success ? 'SUCCESS' : 'FAILED'}\n\n` +
`Output:\n` +
`\`\`\`\n${result.rawOutput || result.message}\n\`\`\`\n\n` +
`Use this output to:\n` +
`- Learn about available commands and their syntax\n` +
`- Execute complex operations not covered by specific tools\n` +
`- Troubleshoot issues with raw CLI access\n` +
`- Discover new features and capabilities`
}
]
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Failed to execute Ludus CLI command: \`${fullCommand}\`\n\n` +
`Error Details:\n` +
`\`\`\`\n${error.message}\n\`\`\`\n\n` +
`Troubleshooting:\n` +
`- Verify the command syntax with: ludus_cli_execute({ command: "help" })\n` +
`- Check if you have the necessary permissions\n` +
`- Ensure the server is properly configured\n` +
`- Try simpler commands first to test connectivity`
}
]
};
}
}
private async handleLudusHelp(args: any) {
const { command, subcommand, user } = args;
// Log the help request
const fullCommand = command && subcommand
? `ludus ${command} ${subcommand} --help`
: command
? `ludus ${command} --help`
: 'ludus help';
this.logger.info('Getting Ludus CLI help', { command: fullCommand, user });
if (!this.ludusCliWrapper) {
return {
content: [
{
type: 'text',
text: `Server not fully initialized yet. Please wait a moment and try again.\n\n` +
`The server is still setting up connections and validating credentials.\n` +
`This usually takes 10-30 seconds after startup.`
}
]
};
}
try {
// Build help command
const helpArgs: string[] = [];
if (command) {
helpArgs.push(command);
if (subcommand) {
helpArgs.push(subcommand);
}
helpArgs.push('--help');
}
// Add user context if provided
if (user) {
helpArgs.push('--user', user);
}
// Execute help command
let result;
if (command) {
// For specific commands: ludus <command> [subcommand] --help
result = await this.ludusCliWrapper.executeArbitraryCommand(command, helpArgs);
} else {
// For general help: ludus --help
result = await this.ludusCliWrapper.executeCommand('--help', []);
}
if (!result.success) {
throw new Error(`Failed to get help: ${result.message}`);
}
// Format response
const targetUser = user || 'current user';
const helpType = command && subcommand
? `${command} ${subcommand}`
: command
? command
: 'general';
return {
content: [
{
type: 'text',
text: `Ludus CLI Help: ${helpType}\n` +
`👤 User: ${targetUser}\n` +
`Command: \`${fullCommand}\`\n\n` +
`\`\`\`\n${result.rawOutput || result.message}\n\`\`\`\n\n` +
`Related Tools:\n` +
`- Use ludus_cli_execute for running discovered commands\n` +
`- Use specific wrapper tools for common operations\n` +
`- Add --help to any wrapper tool for command-specific help`
}
]
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Failed to get Ludus CLI help: ${error.message}\n\n` +
`Troubleshooting:\n` +
`- Verify the command exists with: ludus_help()\n` +
`- Check if you have the necessary permissions\n` +
`- Ensure the server is properly configured\n` +
`- Try: ludus_help({ command: "range" }) for specific help`
}
]
};
}
}
private async handleListAllUsers(args: any) {
this.logger.info('Listing all users');
try {
const result = await handleListAllUsers(args, this.logger, this.ludusCliWrapper!);
return {
content: [
{
type: 'text',
text: result.success
? `All Users Retrieved\n\n` +
`**User Data:**\n` +
`\`\`\`json\n${JSON.stringify(result.users, null, 2)}\n\`\`\`\n\n` +
`**Usage Notes:**\n` +
result.usage?.map((note: string) => `- ${note}`).join('\n') || ''
: `Failed to list all users: ${result.message}\n\n` +
`**Troubleshooting:**\n` +
result.troubleshooting?.map((tip: string) => `- ${tip}`).join('\n') || ''
}
]
};
} catch (error: any) {
this.logger.error('Failed to list all users', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to list all users: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Ensure you have admin privileges\n` +
`- Check if the Ludus server is accessible\n` +
`- Verify admin operations connectivity is working\n` +
`- Try: ludus_help({ command: "users", subcommand: "list" }) for more options`
}
]
};
}
}
private async handleReadRangeConfig(args: any) {
this.logger.info('Reading range configuration', { args });
try {
const result = await handleReadRangeConfig(args, this.logger);
return {
content: [
{
type: 'text',
text: result.success
? `Range Configuration Read Successfully\n\n` +
`**Source:** ${result.source}\n` +
`📏 **Content Length:** ${result.contentLength} characters\n\n` +
`**Configuration Content:**\n` +
`\`\`\`yaml\n${result.content}\n\`\`\`\n\n` +
`**Next Steps:**\n` +
`- Use validate_range_config to check schema compliance\n` +
`- Use write_range_config to save modifications\n` +
`- Use deploy_range to deploy this configuration`
: `Failed to read configuration: ${result.message}\n\n` +
`**Troubleshooting:**\n` +
`- Verify the directory path exists (relative to ~/.ludus-mcp/range-config-templates/)\n` +
`- Use relative paths only (e.g., "base-configs", "user1")\n` +
`- Check directory permissions\n` +
`- Ensure path points to a directory, not a file\n` +
`- Make sure path is within allowed security boundaries`
}
]
};
} catch (error: any) {
this.logger.error('Failed to read range config', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to read range configuration: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Check if the file exists and is readable\n` +
`- Verify network connectivity for URLs\n` +
`- Ensure proper file permissions\n` +
`- Try using absolute file paths`
}
]
};
}
}
private async handleWriteRangeConfig(args: any) {
this.logger.info('Writing range configuration', {
filePath: args.filePath,
contentLength: args.content?.length
});
try {
const result = await handleWriteRangeConfig(args, this.logger);
return {
content: [
{
type: 'text',
text: result.success
? `Range Configuration Saved Successfully\n\n` +
`**File Path:** ${result.filePath}\n` +
`📏 **Content Length:** ${result.contentLength} characters\n` +
`**Schema Validation:** ${result.validation.valid ? 'PASSED' : 'FAILED'}\n` +
(result.validation.warnings.length > 0 ?
`**Warnings:** ${result.validation.warnings.length}\n` +
result.validation.warnings.map((w: string) => `- ${w}`).join('\n') + '\n'
: '') +
`\n**Next Steps:**\n` +
`- Configuration is ready for deployment\n` +
`- Use deploy_range to deploy this configuration\n` +
`- Use read_range_config to review the saved file`
: `Failed to write configuration: ${result.message}\n\n` +
(result.validation ?
`**Validation Errors:**\n` +
result.validation.errors.map((e: string) => `- ${e}`).join('\n') + '\n\n'
: '') +
`**Troubleshooting:**\n` +
`- Fix schema validation errors above\n` +
`- Check YAML syntax is valid\n` +
`- Ensure directory permissions for file path\n` +
`- Use validate_range_config to check config first`
}
]
};
} catch (error: any) {
this.logger.error('Failed to write range config', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to write range configuration: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Check directory permissions\n` +
`- Ensure valid YAML syntax\n` +
`- Verify file path is writable\n` +
`- Use validate_range_config first to check schema`
}
]
};
}
}
private async handleValidateRangeConfig(args: any) {
this.logger.info('Validating range configuration', { args });
try {
const result = await handleValidateRangeConfig(args, this.logger);
return {
content: [
{
type: 'text',
text: result.success
? `Range Configuration Validation Results\n\n` +
`**Source:** ${result.source}\n` +
`**Valid:** ${result.validation.valid ? 'YES' : 'NO'}\n` +
`**Errors:** ${result.validation.errors.length}\n` +
`**Warnings:** ${result.validation.warnings.length}\n\n` +
(result.validation.errors.length > 0 ?
`**Validation Errors:**\n` +
result.validation.errors.map((e: string) => `- ${e}`).join('\n') + '\n\n'
: '') +
(result.validation.warnings.length > 0 ?
`**Warnings:**\n` +
result.validation.warnings.map((w: string) => `- ${w}`).join('\n') + '\n\n'
: '') +
`**Next Steps:**\n` +
(result.validation.valid ?
`- Configuration is valid and ready for deployment\n` +
`- Use deploy_range to deploy this configuration\n` +
`- Use write_range_config to save any modifications`
: `- Fix the validation errors above\n` +
`- Use write_range_config to save corrected version\n` +
`- Re-validate before deployment`)
: `Failed to validate configuration: ${result.message}\n\n` +
`**Troubleshooting:**\n` +
`- Check if the source file/URL is accessible\n` +
`- Use relative paths only (e.g., "base-configs/file.yml", "user1/config.yaml")\n` +
`- Verify YAML syntax is correct\n` +
`- Ensure proper file permissions`
}
]
};
} catch (error: any) {
this.logger.error('Failed to validate range config', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to validate range configuration: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Check if the file exists and is readable\n` +
`- Verify network connectivity for URLs\n` +
`- Ensure valid YAML syntax\n` +
`- Try using absolute file paths`
}
]
};
}
}
private async handleListRangeConfigs(args: any) {
this.logger.info('Listing range configurations', { directory: args.directory });
try {
const result = await handleListRangeConfigs(args, this.logger);
return {
content: [
{
type: 'text',
text: result.success
? (result.smartSearch
? `Smart Search Results\n\n` +
`**Summary:**\n` +
`- Total configs found: ${result.totalCount}\n` +
`- Valid configs: ${result.validCount}\n` +
`- Invalid configs: ${result.totalCount - result.validCount}\n\n` +
`**Search Results:**\n` +
result.searchResults.map((sr: any) =>
sr.found
? `${sr.directory} - Found ${sr.count} config${sr.count === 1 ? '' : 's'}`
: `${sr.directory} - ${sr.reason}`
).join('\n') + '\n\n' +
(result.configs.length > 0 ?
`**Configuration Files:**\n` +
result.configs.map((config: any) =>
`${config.status} **${config.relativePath}** (${config.sizeKB}) - ${config.statusMessage}\n` +
` Full Path: ${config.filePath}\n` +
(config.validation.errors.length > 0 ?
` Errors: ${config.validation.errors.slice(0, 2).join(', ')}${config.validation.errors.length > 2 ? '...' : ''}\n`
: '') +
(config.validation.warnings.length > 0 ?
` Warnings: ${config.validation.warnings.slice(0, 2).join(', ')}${config.validation.warnings.length > 2 ? '...' : ''}\n`
: '')
).join('\n') + '\n\n'
: '') +
`**Next Steps:**\n` +
`- Use read_range_config with file path to examine specific configs\n` +
`- Use validate_range_config to see detailed validation results\n` +
`- Use write_range_config to fix invalid configurations\n` +
`- Use deploy_range to deploy valid configurations` +
(result.message ? `\n\n${result.message}` : '')
: `Range Configurations in ${result.directory}\n\n` +
`**Summary:**\n` +
`- Total configs: ${result.totalCount}\n` +
`- Valid configs: ${result.validCount}\n` +
`- Invalid configs: ${result.totalCount - result.validCount}\n\n` +
(result.configs.length > 0 ?
`**Configuration Files:**\n` +
result.configs.map((config: any) =>
`${config.status} **${config.relativePath}** (${config.sizeKB}) - ${config.statusMessage}\n` +
` Full Path: ${config.filePath}\n` +
(config.validation.errors.length > 0 ?
` Errors: ${config.validation.errors.slice(0, 2).join(', ')}${config.validation.errors.length > 2 ? '...' : ''}\n`
: '') +
(config.validation.warnings.length > 0 ?
` Warnings: ${config.validation.warnings.slice(0, 2).join(', ')}${config.validation.warnings.length > 2 ? '...' : ''}\n`
: '')
).join('\n') + '\n\n'
: '') +
`**Next Steps:**\n` +
`- Use read_range_config with file path to examine specific configs\n` +
`- Use validate_range_config to see detailed validation results\n` +
`- Use write_range_config to fix invalid configurations\n` +
`- Use deploy_range to deploy valid configurations` +
(result.message ? `\n\n${result.message}` : ''))
: `Failed to list configurations: ${result.message}\n\n` +
(result.allowedPaths ?
`**Security Notice:** Directory access is restricted to:\n` +
result.allowedPaths.map((p: string) => `- ${p}`).join('\n') + '\n\n'
: '') +
`**Troubleshooting:**\n` +
`- Verify the directory path exists\n` +
`- Check directory permissions\n` +
`- Ensure path points to a directory, not a file\n` +
`- Make sure path is within allowed security boundaries\n` +
`- Try using absolute paths`
}
]
};
} catch (error: any) {
this.logger.error('Failed to list range configs', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to list range configurations: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Check if the directory exists and is readable\n` +
`- Verify directory permissions\n` +
`- Ensure proper file system access\n` +
`- Use relative paths only (e.g., "base-configs", "user1")\n` +
`- Omit directory parameter to search all templates automatically`
}
]
};
}
}
private async handleGetRangeConfig(args: any) {
this.logger.info('Getting range configuration', { args });
if (!this.ludusCliWrapper) {
return {
content: [
{
type: 'text',
text: `Server not fully initialized yet. Please wait a moment and try again.\n\n` +
`The server is still setting up connections and validating credentials.\n` +
`This usually takes 10-30 seconds after startup.`
}
]
};
}
try {
const result = await handleGetRangeConfig(args, this.logger, this.ludusCliWrapper);
if (result.success) {
const configType = result.example ? 'Example Configuration' : `Range Configuration for ${result.user}`;
return {
content: [
{
type: 'text',
text: `${configType} Retrieved Successfully\n\n` +
`**Configuration Content:**\n` +
`\`\`\`yaml\n${result.content}\n\`\`\`\n\n` +
` **Next Steps:**\n` +
`- Use write_range_config to save modifications\n` +
`- Use validate_range_config to check schema compliance\n` +
`- Use set_range_config to apply this configuration\n` +
`- Use deploy_range to deploy this configuration`
}
]
};
} else {
return {
content: [
{
type: 'text',
text: `Failed to get range configuration\n\n` +
`**Error Details:**\n` +
`\`\`\`\n${result.error}\n\`\`\`\n\n` +
`**Troubleshooting:**\n` +
`- Check if a range configuration exists\n` +
`- Verify user permissions\n` +
`- Try using get_range_config with example=true for a sample config\n` +
`- Use list_range_configs to see available templates`
}
]
};
}
} catch (error: any) {
this.logger.error('Failed to get range config', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to get range configuration: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Verify Ludus CLI connectivity\n` +
`- Check user permissions\n` +
`- Ensure the range is properly configured\n` +
`- Try using the example flag for sample configurations`
}
]
};
}
}
private async handleSetRangeConfig(args: any) {
this.logger.info('Setting range configuration', { args });
if (!this.ludusCliWrapper) {
return {
content: [
{
type: 'text',
text: `Server not fully initialized yet. Please wait a moment and try again.\n\n` +
`The server is still setting up connections and validating credentials.\n` +
`This usually takes 10-30 seconds after startup.`
}
]
};
}
try {
const result = await handleSetRangeConfig(args, this.logger, this.ludusCliWrapper);
return result;
} catch (error: any) {
this.logger.error('Failed to set range config', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to set range configuration: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Verify the configuration file exists and is valid\n` +
`- Check Ludus CLI connectivity\n` +
`- Ensure proper user permissions\n` +
`- Try validating the config file first with validate_range_config`
}
]
};
}
}
private async handleLudusPower(args: any) {
this.logger.info('Executing power management command', { action: args.action, user: args.user });
if (!this.ludusCliWrapper) {
return {
content: [
{
type: 'text',
text: `Server not fully initialized yet. Please wait a moment and try again.\n\n` +
`The server is still setting up connections and validating credentials.\n` +
`This usually takes 10-30 seconds after startup.`
}
]
};
}
try {
const result = await handleLudusPower(args, this.logger, this.ludusCliWrapper);
if (result.success) {
const actionEmoji = result.action === 'on' ? '🟢' : '🔴';
const actionText = result.action === 'on' ? 'powered on' : 'powered off';
let responseText = `${actionEmoji} Range VMs successfully ${actionText} for ${result.user}\n\n`;
if (result.nextSteps && result.nextSteps.length > 0) {
responseText += `Next Steps:\n`;
result.nextSteps.forEach((step: string) => {
responseText += `- ${step}\n`;
});
}
return {
content: [
{
type: 'text',
text: responseText
}
]
};
} else {
if (result.confirmationRequired) {
let confirmText = `${result.message}\n\n`;
confirmText += `Reason: ${result.reason}\n\n`;
confirmText += `To proceed:\n`;
result.instructions.forEach((instruction: string) => {
confirmText += `- ${instruction}\n`;
});
return {
content: [
{
type: 'text',
text: confirmText
}
]
};
} else {
throw new Error(result.message);
}
}
} catch (error: any) {
this.logger.error('Power management failed', { error: error.message });
return {
content: [
{
type: 'text',
text: `P