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.`
}]
};
}
}