UNPKG

ludus-mcp

Version:

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

291 lines (257 loc) 10.6 kB
import { z } from 'zod'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; import { Logger } from '../utils/logger.js'; import { LudusCliWrapper } from '../ludusMCP/cliWrapper.js'; const LudusEnvironmentGuidesSchema = z.object({ action: z.enum(['search', 'read', 'list']).describe('Action to perform: search guides content, read specific guide, or list available guides'), search_query: z.string().optional().describe('Text to search for in environment guides (case-insensitive)'), guide_name: z.string().optional().describe('Specific guide file to read (e.g., "basic-ad-network.md")'), max_results: z.number().optional().default(10).describe('Maximum number of search results to return (default: 10)'), }); export function createLudusEnvironmentGuidesSearchTool(logger: Logger, ludusCliWrapper: LudusCliWrapper) { return { name: 'ludus_environment_guides_search', description: `**ENVIRONMENT GUIDES SHORTCUT** - Direct Access to Ludus Environment Setup Guides **SPECIALIZED TOOL** for immediate access to complete lab environment setup guides without broad searching. **DIRECT ACCESS TO:** - Active Directory network setups - Malware analysis environments - SCCM lab configurations - Kubernetes and container environments - Windows and Linux lab setups - Network topology examples - VM specifications and requirements **ACTIONS:** - \`search\`: Search within all environment guides for specific terms - \`read\`: Read a specific environment guide completely - \`list\`: List all available environment guide files **Usage Examples:** - Find AD guides: \`{"action": "search", "search_query": "Active Directory"}\` - Read specific guide: \`{"action": "read", "guide_name": "basic-ad-network.md"}\` - List all guides: \`{"action": "list"}\` **PERFECT FOR:** - Range planning (ludus_range_planner step 3-4) - Environment architecture research - Understanding deployment patterns - VM and networking requirements **📍 TARGET**: Focuses specifically on \`docs/environment-guides/\` directory and all contained guides.`, inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['search', 'read', 'list'], description: 'Action to perform: search guides content, read specific guide, or list available guides' }, search_query: { type: 'string', description: 'Text to search for in environment guides (case-insensitive)' }, guide_name: { type: 'string', description: 'Specific guide file to read (e.g., "basic-ad-network.md")' }, max_results: { type: 'number', default: 10, description: 'Maximum number of search results to return (default: 10)' } }, required: ['action'] }, }; } export async function handleLudusEnvironmentGuidesSearch( args: z.infer<typeof LudusEnvironmentGuidesSchema>, logger: Logger, ludusCliWrapper: LudusCliWrapper ): Promise<{ content: Array<{ type: string; text: string }> }> { logger.info('Ludus environment guides search requested', { action: args.action, query: args.search_query }); const ludusConfigDir = path.join(os.homedir(), '.ludus-mcp'); const docsDir = path.join(ludusConfigDir, 'docs'); const guidesDir = path.join(docsDir, 'environment-guides'); // Check if docs directory exists try { await fs.access(guidesDir); } catch { return { content: [{ type: 'text', text: `Environment guides not available. The directory was not found at ${guidesDir}.\n\nTry restarting the MCP server to download the documentation.` }] }; } try { switch (args.action) { case 'list': return await handleListGuides(guidesDir, logger); case 'search': if (!args.search_query) { return { content: [{ type: 'text', text: 'Search query is required when action is "search"' }] }; } return await handleSearchGuides(guidesDir, args.search_query, args.max_results, logger); case 'read': if (!args.guide_name) { return { content: [{ type: 'text', text: 'Guide name is required when action is "read". Use "list" action to see available guides.' }] }; } return await handleReadGuide(guidesDir, args.guide_name, logger); default: return { content: [{ type: 'text', text: 'Invalid action. Use "search", "read", or "list"' }] }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error('Ludus environment guides search failed', { error: errorMessage, args }); return { content: [{ type: 'text', text: `Error accessing environment guides: ${errorMessage}` }] }; } } async function handleListGuides(guidesDir: string, logger: Logger) { try { const files = await fs.readdir(guidesDir); const guideFiles = files.filter(file => file.endsWith('.md') || file.endsWith('.mdx')); if (guideFiles.length === 0) { return { content: [{ type: 'text', text: `No environment guide files found in ${guidesDir}` }] }; } // Try to get brief description from each file const guidesWithDescriptions = await Promise.all( guideFiles.map(async (file) => { try { const filePath = path.join(guidesDir, file); const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); // Look for title or first non-empty line as description let description = 'Environment setup guide'; for (const line of lines.slice(0, 10)) { const trimmed = line.trim(); if (trimmed.startsWith('# ')) { description = trimmed.replace('# ', ''); break; } if (trimmed.length > 20 && !trimmed.startsWith('#') && !trimmed.startsWith('```')) { description = trimmed.length > 80 ? trimmed.substring(0, 80) + '...' : trimmed; break; } } return `• **${file}** - ${description}`; } catch { return `• **${file}** - Environment setup guide`; } }) ); return { content: [{ type: 'text', text: `**ENVIRONMENT GUIDES AVAILABLE** (${guideFiles.length} guides)\n\n${guidesWithDescriptions.join('\n')}\n\nUse \`{"action": "read", "guide_name": "filename.md"}\` to read a specific guide.` }] }; } catch (error) { logger.error('Failed to list guides', { error }); return { content: [{ type: 'text', text: `Could not list guide files: ${error instanceof Error ? error.message : String(error)}` }] }; } } async function handleSearchGuides(guidesDir: string, query: string, maxResults: number, logger: Logger) { try { const files = await fs.readdir(guidesDir); const guideFiles = files.filter(file => file.endsWith('.md') || file.endsWith('.mdx')); const queryLower = query.toLowerCase(); const allMatches: Array<{file: string, lineNumber: number, content: string, context: string}> = []; for (const file of guideFiles) { const filePath = path.join(guidesDir, file); const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.toLowerCase().includes(queryLower)) { // Get context around the match const contextStart = Math.max(0, i - 2); const contextEnd = Math.min(lines.length - 1, i + 2); const context = lines.slice(contextStart, contextEnd + 1).join('\n').trim(); allMatches.push({ file, lineNumber: i + 1, content: line.trim(), context: context.length > 250 ? context.substring(0, 250) + '...' : context }); if (allMatches.length >= maxResults) break; } } if (allMatches.length >= maxResults) break; } if (allMatches.length === 0) { return { content: [{ type: 'text', text: `**NO MATCHES** for "${query}" in environment guides.\n\nTry searching for:\n• Environment types (e.g., "AD", "malware", "kubernetes")\n• Technologies (e.g., "windows", "linux", "sccm")\n• Setup terms (e.g., "install", "configure", "deploy")` }] }; } const resultText = allMatches.map((match, index) => `**[${index + 1}] ${match.file} - Line ${match.lineNumber}:**\n\`${match.content}\`\n\n**Context:**\n${match.context}\n` ).join('\n---\n\n'); return { content: [{ type: 'text', text: `**ENVIRONMENT GUIDES SEARCH RESULTS** for "${query}" (${allMatches.length} matches)\n\n${resultText}` }] }; } catch (error) { logger.error('Failed to search guides', { error, query }); return { content: [{ type: 'text', text: `Could not search environment guides: ${error instanceof Error ? error.message : String(error)}` }] }; } } async function handleReadGuide(guidesDir: string, guideName: string, logger: Logger) { try { const filePath = path.join(guidesDir, guideName); const content = await fs.readFile(filePath, 'utf-8'); return { content: [{ type: 'text', text: `**ENVIRONMENT GUIDE: ${guideName}**\n\n${content}\n\n---\n\n**Tip**: Use \`{"action": "search", "search_query": "term"}\` to find specific information across all guides.` }] }; } catch (error) { logger.error('Failed to read guide', { error, guideName }); return { content: [{ type: 'text', text: `Could not read guide "${guideName}": ${error instanceof Error ? error.message : String(error)}\n\nUse \`{"action": "list"}\` to see available guide files.` }] }; } }