UNPKG

ludus-mcp

Version:

MCP server for managing Ludus cybersecurity training environments through natural language commands

230 lines (194 loc) 7.44 kB
import { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; interface RangeConfigSchemaSearch { search_terms?: string[]; include_full_schema?: boolean; section_filter?: string[]; } export const ludusReadRangeConfigSchemaTool: Tool = { name: 'ludus_read_range_config_schema', description: '**RANGE CONFIG SCHEMA** - Direct access to official Ludus range configuration JSON schema. Provides complete structure, validation rules, and property definitions for range configs.', inputSchema: { type: 'object', properties: { search_terms: { type: 'array', items: { type: 'string' }, description: 'Optional array of terms to search for within the schema (case-insensitive partial matches)' }, include_full_schema: { type: 'boolean', description: 'If true, returns the complete schema. If false and search_terms provided, returns filtered results.', default: false }, section_filter: { type: 'array', items: { type: 'string', enum: ['properties', 'definitions', 'required', 'examples', 'enum'] }, description: 'Filter to specific schema sections (properties, definitions, required, examples, enum)' } } } }; export async function handleLudusReadRangeConfigSchema(args: any): Promise<CallToolResult> { try { // Try to read the schema file const homeDir = os.homedir(); const schemaFilePath = path.join(homeDir, '.ludus-mcp', 'schemas', 'range-config.json'); let schemaContent; try { const fileContent = await fs.readFile(schemaFilePath, 'utf-8'); schemaContent = JSON.parse(fileContent); } catch (fileError) { return { content: [ { type: 'text', text: `**Range Config Schema Not Available** Could not read the range configuration schema from: ${schemaFilePath} **Error**: ${fileError instanceof Error ? fileError.message : String(fileError)} **To Fix**: 1. Restart the MCP server to trigger schema download 2. Check your internet connection (schema is downloaded from docs.ludus.cloud) 3. Verify the ~/.ludus-mcp/schemas/ directory exists **Alternative**: Use the \`validate_range_config\` tool which has built-in schema validation.` } ] }; } const { search_terms, include_full_schema = false, section_filter } = args; // If full schema requested or no filters, return everything if (include_full_schema || (!search_terms && !section_filter)) { return { content: [ { type: 'text', text: `**LUDUS RANGE CONFIG SCHEMA** (Complete) **Source**: ${schemaFilePath} **Schema Version**: ${schemaContent.$schema || 'Unknown'} **Title**: ${schemaContent.title || 'Ludus Range Configuration'} \`\`\`json ${JSON.stringify(schemaContent, null, 2)} \`\`\` **Tips**: - Use \`search_terms\` to find specific properties - Use \`section_filter\` to focus on specific parts - Check \`required\` arrays for mandatory fields - Look at \`enum\` values for valid options` } ] }; } // Apply filtering let filteredContent: any = {}; let resultsFound = 0; // Section filtering if (section_filter && section_filter.length > 0) { for (const section of section_filter) { if (schemaContent[section]) { filteredContent[section] = schemaContent[section]; resultsFound++; } } } else { filteredContent = schemaContent; } // Search terms filtering if (search_terms && search_terms.length > 0) { const searchResults = searchInSchema(filteredContent, search_terms); if (searchResults.matches.length === 0) { return { content: [ { type: 'text', text: `**NO MATCHES** for search terms: ${search_terms.join(', ')} **Searched in**: Range config schema **Suggestions**: - Try broader terms like: "ludus", "network", "windows", "roles", "defaults" - Check property names like: "vm_name", "hostname", "template", "cpus", "ram_gb" - Look for validation terms like: "required", "enum", "type", "minimum", "maximum" **Use \`include_full_schema: true\` to see the complete schema structure.**` } ] }; } return { content: [ { type: 'text', text: `**RANGE CONFIG SCHEMA SEARCH** (${search_terms.join(', ')}) **Found ${searchResults.matches.length} matches:** ${searchResults.matches.map((match, index) => `**[${index + 1}] ${match.path}**\n\`\`\`json\n${JSON.stringify(match.value, null, 2)}\n\`\`\`` ).join('\n\n')} ${searchResults.context.length > 0 ? `\n**Related Context:**\n${searchResults.context.join('\n')}` : ''} **Tips**: - Use exact property names for precise matches - Check \`enum\` arrays for valid values - Look at \`description\` fields for usage guidance` } ] }; } // Section filter only (no search terms) return { content: [ { type: 'text', text: `**RANGE CONFIG SCHEMA** (${section_filter?.join(', ') || 'Filtered'}) \`\`\`json ${JSON.stringify(filteredContent, null, 2)} \`\`\` **Use search_terms to find specific properties within these sections.**` } ] }; } catch (error) { return { content: [ { type: 'text', text: `**Schema Read Error**: ${error instanceof Error ? error.message : String(error)}` } ] }; } } function searchInSchema(schema: any, searchTerms: string[]): { matches: Array<{path: string, value: any}>, context: string[] } { const matches: Array<{path: string, value: any}> = []; const context: string[] = []; function searchRecursive(obj: any, currentPath: string = '') { if (typeof obj === 'object' && obj !== null) { for (const [key, value] of Object.entries(obj)) { const newPath = currentPath ? `${currentPath}.${key}` : key; // Check if key or value matches any search term const keyMatches = searchTerms.some(term => key.toLowerCase().includes(term.toLowerCase()) ); const valueMatches = searchTerms.some(term => { if (typeof value === 'string') { return value.toLowerCase().includes(term.toLowerCase()); } return false; }); if (keyMatches || valueMatches) { matches.push({ path: newPath, value }); // Add context if it's a description or has useful metadata if (typeof value === 'object' && value !== null && 'description' in value && typeof value.description === 'string') { context.push(`${newPath}: ${value.description}`); } } // Recurse into nested objects if (typeof value === 'object') { searchRecursive(value, newPath); } } } } searchRecursive(schema); return { matches: matches.slice(0, 20), context: context.slice(0, 5) }; // Limit results }