#!/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: `Power management failed: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Verify the user has a deployed range\n` +
`- Check if you have admin permissions (if managing other users)\n` +
`- Ensure the range exists and is accessible\n` +
`- Try get_range_status() to check current range state\n` +
`- Use ludus_help() for more information`
}
]
};
}
}
private async handleGetCredentialFromUser(args: any) {
this.logger.info('Prompting user for credential entry', { credName: args.credName });
try {
const result = await handleGetCredentialFromUser(args, this.logger);
if (result.success) {
// Handle help mode
if (result.help) {
return {
content: [
{
type: 'text',
text: result.content
}
]
};
}
// Normal success mode
let responseText = `Credential "${result.credName}" stored securely\n\n`;
responseText += `Location: ${result.location}\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.cancelled) {
return {
content: [
{
type: 'text',
text: `Credential entry cancelled by user for "${result.credName}"\n\n` +
`You can try again when ready with the same command.`
}
]
};
} else {
throw new Error(result.message);
}
}
} catch (error: any) {
this.logger.error('Credential entry failed', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to store credential: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Ensure GUI access is available for popup dialogs\n` +
`- Check credential name follows LudusCredName-<TargetUser>-<CredName> format\n` +
`- Verify OS credential manager is functional\n` +
`- Try get_credential_from_user({ help: true }) for more information`
}
]
};
}
}
private async handleInsertCredsRangeConfig(args: any) {
this.logger.info('Processing credential injection for range config', {
configPath: args.configPath,
credentialCount: args.credentialMappings ? Object.keys(args.credentialMappings).length : 0
});
try {
const result = await handleInsertCredsRangeConfig(args, this.logger);
if (result.success) {
// Handle help mode
if (result.help) {
let helpText = `${result.description}\n\n`;
helpText += `**Purpose:** ${result.usage.purpose}\n\n`;
helpText += `**Workflow:**\n`;
result.usage.workflow.forEach((step: string) => {
helpText += `${step}\n`;
});
helpText += `\n**Required Parameters:**\n`;
Object.entries(result.usage.requiredParameters).forEach(([param, desc]) => {
helpText += `- ${param}: ${desc}\n`;
});
helpText += `\n**Optional Parameters:**\n`;
Object.entries(result.usage.optionalParameters).forEach(([param, desc]) => {
helpText += `- ${param}: ${desc}\n`;
});
helpText += `\n**Example Usage:**\n`;
helpText += `configPath: "${result.usage.exampleUsage.configPath}"\n`;
helpText += `credentialMappings:\n`;
Object.entries(result.usage.exampleUsage.credentialMappings).forEach(([placeholder, credName]) => {
helpText += ` "${placeholder}": "${credName}"\n`;
});
helpText += `\n**Formats:**\n`;
helpText += `- Placeholder Format: ${result.usage.placeholderFormat}\n`;
helpText += `- Credential Format: ${result.usage.credentialFormat}\n`;
helpText += `\n**Path Resolution:**\n`;
helpText += `- Relative paths: ${result.usage.pathResolution.relativePaths}\n`;
helpText += `- Absolute paths: ${result.usage.pathResolution.absolutePaths}\n`;
helpText += `\n**Security:**\n`;
result.usage.security.forEach((note: string) => {
helpText += `- ${note}\n`;
});
return {
content: [
{
type: 'text',
text: helpText
}
]
};
}
// Normal success mode
let responseText = `Credential injection completed successfully\n\n`;
responseText += `Config Path: ${result.configPath}\n`;
responseText += `Credentials Injected: ${result.credentialsInjected}\n`;
responseText += `Validation: ${result.validation.valid ? 'PASSED' : 'FAILED'}\n\n`;
if (result.validation.warnings && result.validation.warnings.length > 0) {
responseText += ` Schema Warnings:\n`;
result.validation.warnings.forEach((warning: string) => {
responseText += `- ${warning}\n`;
});
responseText += `\n`;
}
if (result.outputPath) {
responseText += `Saved to: ${result.outputPath}\n\n`;
}
responseText += `Security Note: ${result.securityNote}\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 {
let errorText = `Credential injection failed: ${result.message}\n\n`;
if (result.missingCredentials && result.missingCredentials.length > 0) {
errorText += `Missing Credentials:\n`;
result.missingCredentials.forEach((cred: string) => {
errorText += `- ${cred}\n`;
});
errorText += `\n`;
}
if (result.errors && result.errors.length > 0) {
errorText += ` Errors:\n`;
result.errors.forEach((error: string) => {
errorText += `- ${error}\n`;
});
errorText += `\n`;
}
if (result.suggestions && result.suggestions.length > 0) {
errorText += ` Suggestions:\n`;
result.suggestions.forEach((suggestion: string) => {
errorText += `- ${suggestion}\n`;
});
}
return {
content: [
{
type: 'text',
text: errorText
}
]
};
}
} catch (error: any) {
this.logger.error('Credential injection failed', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to inject credentials: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Verify config file path exists using list_range_configs\n` +
`- Check credential names match stored credentials exactly\n` +
`- Ensure credential mappings match placeholders in config file\n` +
`- Verify OS credential manager access is working`
}
]
};
}
}
private async handleLudusDocsSearch(args: any) {
this.logger.info('Searching Ludus documentation', { action: args.action, query: args.search_query });
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 {
return await handleLudusDocsSearch(args, this.logger, this.ludusCliWrapper);
} catch (error: any) {
this.logger.error('Failed to search documentation', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to search documentation: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Check if the action and parameters are valid\n` +
`- Ensure the documentation was downloaded during server startup\n` +
`- Try restarting the server to re-download documentation`
}
]
};
}
}
private async handleLudusRangePlanner(args: any) {
this.logger.info('Executing Ludus Range Planner', { args });
try {
const result = await handleLudusRangePlanner(args, this.logger);
if (result.success) {
let responseText = `**Range Planning Completed Successfully**\n\n`;
responseText += `**Plan Details:**\n`;
responseText += `• Plan ID: ${result.plan_id}\n`;
responseText += `• Estimated VMs: ${result.estimated_vms}\n\n`;
responseText += `${result.message}\n\n`;
responseText += ` **Structured Instructions:**\n`;
responseText += `Follow these steps using your existing MCP tools:\n\n`;
responseText += `**Research Phase:**\n`;
responseText += `1. ${result.instructions.research_phase.step1_analyze_intent.description}\n`;
responseText += `2. ${result.instructions.research_phase.step2_clarify_requirements.description}\n`;
responseText += `3. ${result.instructions.research_phase.step3_research_components.description}\n`;
responseText += `4. ${result.instructions.research_phase.step4_handle_missing_roles.description}\n`;
if (result.instructions.research_phase.step5_validate_role_variables) {
responseText += `5. ${result.instructions.research_phase.step5_validate_role_variables.description}\n\n`;
}
responseText += `**Design Phase:**\n`;
responseText += `6. ${result.instructions.design_phase.step6_design_architecture.description}\n`;
responseText += `7. ${result.instructions.design_phase.step7_generate_config.description}\n`;
responseText += `8. ${result.instructions.design_phase.step8_qa_check_against_plan.description}\n\n`;
responseText += `**Validation Phase:**\n`;
responseText += `9. ${result.instructions.validation_phase.step9_validate_config.description}\n`;
responseText += `10. ${result.instructions.validation_phase.step10_save_and_document.description}\n\n`;
responseText += `**Remember:** Use validate_range_config with content parameter before writing files!`;
return {
content: [
{
type: 'text',
text: responseText
}
]
};
} else {
return {
content: [
{
type: 'text',
text: `Range planning failed: ${result.message}\n\n` +
`Please check your input parameters and try again.`
}
]
};
}
} catch (error: any) {
this.logger.error('Ludus Range Planner failed', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to execute Ludus Range Planner: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Check the user_intent parameter is provided\n` +
`- Verify all optional parameters are valid\n` +
`- Try ludus_help() for more information on usage`
}
]
};
}
}
private async handleLudusRolesDocsRead(args: any) {
this.logger.info('Reading Ludus roles docs');
try {
return await handleLudusRolesDocsRead(args);
} catch (error: any) {
this.logger.error('Failed to read roles docs', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to read roles docs: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Check if the action and parameters are valid\n` +
`- Ensure the documentation was downloaded during server startup\n` +
`- Try restarting the server to re-download documentation`
}
]
};
}
}
private async handleLudusEnvironmentGuidesSearch(args: any) {
this.logger.info('Searching Ludus environment guides', { action: args.action, query: args.search_query });
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 {
return await handleLudusEnvironmentGuidesSearch(args, this.logger, this.ludusCliWrapper);
} catch (error: any) {
this.logger.error('Failed to search environment guides', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to search environment guides: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Check if the action and parameters are valid\n` +
`- Ensure the documentation was downloaded during server startup\n` +
`- Try restarting the server to re-download documentation`
}
]
};
}
}
private async handleLudusNetworkingDocsRead(args: any) {
this.logger.info('Searching Ludus networking docs', { action: args.action, query: args.search_query });
try {
return await handleLudusNetworkingDocsRead(args);
} catch (error: any) {
this.logger.error('Failed to read networking docs', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to read networking docs: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Check if the action and parameters are valid\n` +
`- Ensure the documentation was downloaded during server startup\n` +
`- Try restarting the server to re-download documentation`
}
]
};
}
}
private async handleLudusReadRangeConfigSchema(args: any) {
this.logger.info('Reading range configuration schema', { args });
try {
return await handleLudusReadRangeConfigSchema(args);
} catch (error: any) {
this.logger.error('Failed to read range configuration schema', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to read range configuration schema: \`${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 handleLudusRangeConfigCheckAgainstPlan(args: any) {
this.logger.info('Range config QA check requested', { user_requirements: args.user_requirements?.substring(0, 100) });
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 {
return await handleLudusRangeConfigCheckAgainstPlan(args, this.logger, this.ludusCliWrapper);
} catch (error: any) {
this.logger.error('Failed to run QA check', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to run QA check: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Check if all required parameters are provided\n` +
`- Ensure user_requirements, config_content, and roles_used are specified`
}
]
};
}
}
private async handleLudusReadRoleCollectionSchema(args: any) {
this.logger.info('Reading role collection schema', { args });
try {
return await handleLudusReadRoleCollectionSchema(args);
} catch (error: any) {
this.logger.error('Failed to read role schema', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to read role collection schema: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Restart the MCP server to reinitialize the schema\n` +
`- Check file system permissions\n` +
`- Verify the schema file was created during server startup`
}
]
};
}
}
private async handleLudusListRoleCollectionSchemas(args: any) {
this.logger.info('Listing role collection schemas', { args });
try {
return await handleLudusListRoleCollectionSchemas(args, this.logger);
} catch (error: any) {
this.logger.error('Failed to list role collection schemas', { error: error.message });
return {
content: [
{
type: 'text',
text: `Failed to list role collection schemas: \`${error.message}\`\n\n` +
`Troubleshooting:\n` +
`- Restart the MCP server to download schemas from GitHub\n` +
`- Check internet connection for GitHub access\n` +
`- Verify ~/.ludus-mcp/schemas/ directory exists\n` +
`- Ensure the repository and schemas are available on main branch`
}
]
};
}
}
private setupErrorHandlers(): void {
this.server.onerror = (error) => {
this.logger.error('Server error', { error });
};
process.on('SIGINT', async () => {
this.logger.info('Received SIGINT, shutting down gracefully');
await this.shutdown();
process.exit(0);
});
process.on('SIGTERM', async () => {
this.logger.info('Received SIGTERM, shutting down gracefully');
await this.shutdown();
process.exit(0);
});
}
/**
* Load configuration from keyring first, then environment variables
*/
private async loadConfigFromAvailableSources(): Promise<LudusConfig | null> {
// Try keyring first if available
if (isKeyringSupportAvailable()) {
try {
const credentials = await getCredentials([
CREDENTIAL_KEYS.ADMIN_USER,
CREDENTIAL_KEYS.CONNECTION_METHOD,
CREDENTIAL_KEYS.WIREGUARD_CONFIG_PATH,
CREDENTIAL_KEYS.API_KEY,
CREDENTIAL_KEYS.SSH_HOST,
CREDENTIAL_KEYS.SSH_USER,
CREDENTIAL_KEYS.SSH_AUTH_METHOD,
CREDENTIAL_KEYS.SSH_PASSWORD,
CREDENTIAL_KEYS.SSH_KEY_PATH,
CREDENTIAL_KEYS.SSH_KEY_PASSPHRASE
]);
const connectionMethod = (credentials[CREDENTIAL_KEYS.CONNECTION_METHOD] as 'wireguard' | 'ssh-tunnel') || 'wireguard';
const sshAuthMethod = (credentials[CREDENTIAL_KEYS.SSH_AUTH_METHOD] as 'password' | 'key') || 'password';
const baseUrl = connectionMethod === 'ssh-tunnel' ? 'https://localhost:8080' : 'https://198.51.100.1:8080';
// Check if we have all required credentials from keyring
const hasBaseCredentials =
credentials[CREDENTIAL_KEYS.ADMIN_USER] &&
credentials[CREDENTIAL_KEYS.CONNECTION_METHOD] &&
credentials[CREDENTIAL_KEYS.API_KEY] &&
credentials[CREDENTIAL_KEYS.SSH_HOST] &&
credentials[CREDENTIAL_KEYS.SSH_USER] &&
credentials[CREDENTIAL_KEYS.SSH_AUTH_METHOD];
const hasWireguardConfig = connectionMethod === 'ssh-tunnel' || credentials[CREDENTIAL_KEYS.WIREGUARD_CONFIG_PATH];
const hasSshAuth = (sshAuthMethod === 'password' && !!credentials[CREDENTIAL_KEYS.SSH_PASSWORD]) ||
(sshAuthMethod !== 'password' && !!credentials[CREDENTIAL_KEYS.SSH_KEY_PATH]);
if (hasBaseCredentials && hasWireguardConfig && hasSshAuth) {
this.logger.info('Using configuration from keyring');
return {
adminUser: credentials[CREDENTIAL_KEYS.ADMIN_USER]!,
connectionMethod,
wireguardConfig: credentials[CREDENTIAL_KEYS.WIREGUARD_CONFIG_PATH] || undefined,
apiKey: credentials[CREDENTIAL_KEYS.API_KEY]!,
sshHost: credentials[CREDENTIAL_KEYS.SSH_HOST]!,
sshUser: credentials[CREDENTIAL_KEYS.SSH_USER]!,
sshAuthMethod,
sshPassword: credentials[CREDENTIAL_KEYS.SSH_PASSWORD] || undefined,
sshKeyPath: credentials[CREDENTIAL_KEYS.SSH_KEY_PATH] || undefined,
sshKeyPassphrase: credentials[CREDENTIAL_KEYS.SSH_KEY_PASSPHRASE] || undefined,
ludusUrl: process.env.LUDUS_URL || baseUrl,
verifySSL: process.env.LUDUS_VERIFY === 'true'
};
} else {
this.logger.warn('Keyring credentials incomplete');
}
} catch (error: any) {
this.logger.debug('Failed to load credentials from keyring', { error: error.message });
}
}
// Fall back to environment variables
const envVars = {
adminUser: process.env.LUDUS_ADMIN_USER,
connectionMethod: process.env.LUDUS_CONNECTION_METHOD,
wireguardConfig: process.env.LUDUS_WIREGUARD_CONFIG,
apiKey: process.env.LUDUS_API_KEY,
sshHost: process.env.LUDUS_SSH_HOST,
sshUser: process.env.LUDUS_SSH_USER,
sshAuthMethod: process.env.LUDUS_SSH_AUTH_METHOD,
sshPassword: process.env.LUDUS_SSH_PASSWORD,
sshKeyPath: process.env.LUDUS_SSH_KEY_PATH
};
const connectionMethod = (envVars.connectionMethod as 'wireguard' | 'ssh-tunnel') || 'wireguard';
const sshAuthMethod = (envVars.sshAuthMethod as 'password' | 'key') || 'password';
const hasBaseEnvVars = envVars.adminUser && envVars.apiKey && envVars.sshHost && envVars.sshUser && envVars.sshAuthMethod;
const hasWireguardConfig = connectionMethod === 'ssh-tunnel' || envVars.wireguardConfig;
const hasSshAuth = (sshAuthMethod === 'password' && envVars.sshPassword) ||
(sshAuthMethod !== 'password' && envVars.sshKeyPath);
if (hasBaseEnvVars && hasWireguardConfig && hasSshAuth) {
this.logger.info('Using configuration from environment variables');
const baseUrl = connectionMethod === 'ssh-tunnel' ? 'https://localhost:8080' : 'https://198.51.100.1:8080';
return {
adminUser: envVars.adminUser!,
connectionMethod,
wireguardConfig: envVars.wireguardConfig || undefined,
apiKey: envVars.apiKey!,
sshHost: envVars.sshHost!,
sshUser: envVars.sshUser!,
sshAuthMethod,
sshPassword: envVars.sshPassword || undefined,
sshKeyPath: envVars.sshKeyPath || undefined,
ludusUrl: process.env.LUDUS_URL || baseUrl,
verifySSL: process.env.LUDUS_VERIFY === 'true'
};
}
return null;
}
private async initializeServer(): Promise<void> {
this.logger.info('Initializing Ludus MCP Server');
// For MCP server, only try to load existing configuration - never run interactive setup
// Interactive setup should only be run via --setup-keyring flag
this.ludusConfig = await this.loadConfigFromAvailableSources();
if (!this.ludusConfig) {
throw new Error('No configuration available. Please run: npx ludus-mcp --setup-keyring');
}
// Download Ludus documentation
try {
await downloadLudusDocumentation(this.logger);
} catch (error) {
this.logger.warn('Failed to download Ludus documentation', { error: error instanceof Error ? error.message : String(error) });
}
// Initialize base configurations (download from GitHub)
try {
await downloadBaseConfigs(this.logger);
} catch (error) {
// Don't fail startup if base configs download fails - just log it
this.logger.warn('Base configs download failed', { error: error instanceof Error ? error.message : String(error) });
}
// Download fresh schemas (GitHub + docs.ludus.cloud)
try {
await downloadSchemas(this.logger);
} catch (error) {
// Don't fail startup if schema download fails - just log it
this.logger.warn('Schema download failed', { error: error instanceof Error ? error.message : String(error) });
}
// Initialize CLI wrapper with configuration
this.ludusCliWrapper = new LudusCliWrapper(this.logger, this.ludusConfig);
// Wait for SSH tunnels to be established if needed
if (this.ludusConfig.connectionMethod === 'ssh-tunnel') {
this.logger.info('Waiting for SSH tunnels to establish...');
// Give tunnels time to establish before testing connectivity
await new Promise(resolve => setTimeout(resolve, 8000));
}
// Test connectivity
this.logger.info('Testing connectivity...');
const connectivity = await this.ludusCliWrapper.testConnectivity();
if (!connectivity.rangeOps) {
this.logger.error('Range operations connectivity test failed');
throw new Error('Range operations connectivity is required but not available');
}
this.logger.info('Connectivity test results', {
rangeOps: connectivity.rangeOps,
adminOps: connectivity.adminOps
});
if (!connectivity.adminOps) {
this.logger.warn('Admin operations connectivity not available - user management will be limited');
}
this.logger.info('Ludus MCP Server initialized successfully');
console.error('Ludus MCP Server running with 27 tools available');
console.error('Range Management: deploy_range, get_range_status, list_user_ranges, get_connection_info, destroy_range, range_abort, ludus_power');
console.error('Configuration Tools: read_range_config, write_range_config, validate_range_config, list_range_configs, get_range_config, set_range_config');
console.error('Utility Tools: get_tags, ludus_cli_execute, ludus_help, list_all_users, get_credential_from_user, insert_creds_range_config, ludus_docs_search');
console.error('Search & Planning: ludus_range_planner, ludus_roles_search, ludus_environment_guides_search, ludus_networking_search, ludus_read_range_config_schema, ludus_range_config_check_against_plan, ludus_read_role_collection_schema');
console.error('Base configs synced: ~/.ludus-mcp/range-config-templates/base-configs/ (from GitHub)');
console.error('Schemas synced: ~/.ludus-mcp/schemas/ (GitHub + docs.ludus.cloud)');
console.error('Documentation cached: ~/.ludus-mcp/ludus-docs/');
}
private async shutdown(): Promise<void> {
this.logger.info('Shutting down Ludus MCP Server');
if (this.ludusCliWrapper) {
await this.ludusCliWrapper.cleanup();
}
await this.server.close();
this.logger.info('Server shutdown complete');
}
async start(): Promise<void> {
try {
// Connect MCP transport first so we can respond to protocol messages
const transport = new StdioServerTransport();
await this.server.connect(transport);
// Then try to initialize - if this fails, we can still respond to MCP requests
try {
await this.initializeServer();
this.logger.info('Ludus MCP Server started successfully');
} catch (initError: any) {
this.logger.error('Server initialization failed, but MCP transport is connected', initError);
// Don't exit - let the server respond to MCP requests with initialization errors
}
} catch (error: any) {
this.logger.error('Failed to start MCP transport', error);
console.error(`\nFailed to start Ludus MCP Server: ${error.message}`);
process.exit(1);
}
}
private setupPromptHandlers(): void {
// List available prompts
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: ALL_PROMPTS
};
});
// Get specific prompt
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const promptName = request.params.name;
if (promptName === 'create-ludus-range') {
return await handleCreateLudusRangePrompt(request.params.arguments as any || {});
}
if (promptName === 'execute-ludus-cmd') {
return await handleExecuteLudusCmdPrompt(request.params.arguments as any || {});
}
throw new Error(`Unknown prompt: ${promptName}`);
});
}
}
// Handle CLI arguments
async function handleCliArguments(): Promise<void> {
const args = process.argv.slice(2);
if (args.includes('--setup-keyring') || args.includes('--renew-keyring')) {
const isRenewal = args.includes('--renew-keyring');
console.error(`\nLudus MCP Server - ${isRenewal ? 'Keyring Renewal' : 'Keyring Setup'}`);
console.error('=====================================');
console.error(`This will ${isRenewal ? 'update your existing' : 'securely store your'} Ludus credentials in the OS keyring.\n`);
const logger = new Logger(isRenewal ? 'KeyringRenewal' : 'KeyringSetup');
const setup = new InteractiveSetup(logger);
try {
const result = await setup.runSetup(isRenewal); // Force interactive if renewing
if (result.success) {
console.error(`\nCredentials successfully ${isRenewal ? 'updated in' : 'stored in'} keyring!`);
console.error('Your credentials are now encrypted and stored securely.');
console.error('You can now start the MCP server without prompting:\n');
console.error(' npx ludus-mcp');
// Add WireGuard guidance if using WireGuard connection method
if (result.config && result.config.connectionMethod === 'wireguard') {
console.error('\n IMPORTANT: WireGuard Usage');
console.error(' • Manually start your WireGuard tunnel before launching Claude Desktop');
console.error(' • For automatic startup, configure WireGuard as a Windows service');
console.error(' • If WireGuard is down, the MCP client will try SSH tunnel fallback');
}
// Determine the path to the built server.js file
const serverJsPath = path.resolve(process.cwd(), 'dist', 'server.js').replace(/\\/g, '/');
console.error('\nClaude Desktop configuration:');
console.error(' Add this to your ~/.claude_desktop_config.json:');
console.error(' {');
console.error(' "mcpServers": {');
console.error(' "ludus": {');
console.error(' "command": "node",');
console.error(` "args": ["${serverJsPath}"]`);
console.error(' }');
console.error(' },');
console.error(' "isUsingBuiltInNodeForMcp": false');
console.error(' }');
console.error('');
console.error(' Note: The API key is still managed by the Ludus CLI.');
console.error(' Make sure to run: ludus apikey');
} else {
console.error(`\n Setup failed: ${result.message}`);
process.exit(1);
}
} catch (error: any) {
console.error(`\n Setup error: ${error.message}`);
process.exit(1);
}
process.exit(0);
}
if (args.includes('--help') || args.includes('-h')) {
console.error('\n Ludus MCP Server');
console.error('===================');
console.error('Secure MCP server for Ludus cybersecurity training environments.\n');
console.error('Usage:');
console.error(' npx ludus-mcp Start the MCP server');
console.error(' npx ludus-mcp --setup-keyring Set up secure credential storage');
console.error(' npx ludus-mcp --renew-keyring Update existing credentials in keyring');
console.error(' npx ludus-mcp --help Show this help message\n');
console.error('Features:');
console.error(' • Secure credential storage using OS keyring');
console.error(' • WireGuard VPN connectivity');
console.error(' • SSH tunnel management for admin operations');
console.error(' • 10 core tools for range management + help + general CLI access\n');
console.error('For more information, visit: https://github.com/badsectorlabs/ludus');
process.exit(0);
}
}
// Handle CLI arguments first
handleCliArguments().then(() => {
// Start the server
const server = new LudusMCPServer();
server.start().catch((error) => {
console.error('Fatal error starting server:', error);
process.exit(1);
});
}).catch((error) => {
console.error('Fatal error:', error);
process.exit(1);
});