import { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import * as yaml from 'js-yaml';
export const ludusReadRoleCollectionSchemaTool: Tool = {
name: 'ludus_read_role_collection_schema',
description: '**ROLE SCHEMA REFERENCE** - Reads Ludus role and collection schemas from individual YAML files. Returns comprehensive role data including variables, descriptions, dependencies, and GitHub repositories. Essential for range planning and configuration validation.',
inputSchema: {
type: 'object',
properties: {
help: {
type: 'boolean',
description: 'Show help information about available roles and collections',
default: false
},
file_names: {
type: 'array',
items: { type: 'string' },
description: 'Optional array of specific YAML file names to read (e.g., ["ludus_ad.yaml", "sccm_collection.yaml"]). If omitted, reads all YAML files in the schemas directory.'
}
}
}
};
/**
* Load all YAML schema files from the schemas directory and aggregate them
*/
async function loadAllYamlSchemas(schemaDir: string, fileNamesFilter?: string[]): Promise<any> {
const aggregatedSchema = {
ludus_roles_schema: {
roles: {} as Record<string, any>,
collections: {} as Record<string, any>,
metadata: {
total_files: 0,
total_roles: 0,
total_collections: 0,
last_updated: new Date().toISOString(),
source_files: [] as string[],
filtered: fileNamesFilter ? true : false,
requested_files: fileNamesFilter || []
}
}
};
try {
// Read all files in the schemas directory
const files = await fs.readdir(schemaDir);
let yamlFiles = files.filter(file =>
file.endsWith('.yaml') || file.endsWith('.yml')
);
// Apply file name filter if provided
if (fileNamesFilter && fileNamesFilter.length > 0) {
yamlFiles = yamlFiles.filter(file => fileNamesFilter.includes(file));
// Check for any requested files that weren't found
const missingFiles = fileNamesFilter.filter(requested =>
!yamlFiles.includes(requested)
);
if (missingFiles.length > 0) {
throw new Error(`Requested files not found: ${missingFiles.join(', ')}. Available files: ${files.filter(f => f.endsWith('.yaml') || f.endsWith('.yml')).join(', ')}`);
}
}
if (yamlFiles.length === 0) {
if (fileNamesFilter && fileNamesFilter.length > 0) {
throw new Error(`No matching YAML files found for filter: ${fileNamesFilter.join(', ')}`);
} else {
throw new Error('No YAML schema files found in schemas directory');
}
}
// Process each YAML file
for (const file of yamlFiles) {
try {
const filePath = path.join(schemaDir, file);
const fileContent = await fs.readFile(filePath, 'utf-8');
const parsedYaml = yaml.load(fileContent) as any;
if (!parsedYaml || typeof parsedYaml !== 'object') {
console.warn(`Skipping invalid YAML file: ${file}`);
continue;
}
// Determine if it's a role or collection based on type field
const itemType = parsedYaml.type || 'role'; // Default to role if not specified
const itemName = parsedYaml.name || path.basename(file, path.extname(file));
if (itemType === 'collection') {
aggregatedSchema.ludus_roles_schema.collections[itemName] = {
...parsedYaml,
source_file: file
};
aggregatedSchema.ludus_roles_schema.metadata.total_collections++;
} else {
aggregatedSchema.ludus_roles_schema.roles[itemName] = {
...parsedYaml,
source_file: file
};
aggregatedSchema.ludus_roles_schema.metadata.total_roles++;
}
aggregatedSchema.ludus_roles_schema.metadata.source_files.push(file);
aggregatedSchema.ludus_roles_schema.metadata.total_files++;
} catch (fileError) {
console.warn(`Error processing YAML file ${file}:`, fileError);
// Continue processing other files
}
}
return aggregatedSchema;
} catch (error) {
throw new Error(`Failed to load YAML schemas: ${error instanceof Error ? error.message : String(error)}`);
}
}
export async function handleLudusReadRoleCollectionSchema(args: any): Promise<CallToolResult> {
try {
const { help = false, file_names } = args;
if (help) {
return {
content: [
{
type: 'text',
text: `**LUDUS ROLE COLLECTION SCHEMA TOOL**
**PURPOSE:**
Reads Ludus role and collection schemas from individual YAML files. Essential reference for range planning and configuration validation.
**WHAT IT RETURNS:**
- Complete inventory of available Ludus roles and collections
- Role variables, types, and requirements for each role
- Collection definitions with bundled roles
- GitHub repository links and documentation
- Author information and version details
**FILE STRUCTURE:**
~/.ludus-mcp/schemas/
├── ludus_ad.yaml
├── ludus_sccm.yaml
├── domain_controller_collection.yaml
└── monitoring_stack.yaml
**USAGE OPTIONS:**
1. **Read All Schemas (Default):**
\`ludus_read_role_collection_schema\`
- Returns complete data from all YAML files
2. **Read Specific Files:**
\`ludus_read_role_collection_schema file_names=["ludus_ad.yaml", "sccm_collection.yaml"]\`
- Returns data only from specified files
- Faster when you need specific roles/collections
- Exact file names required (including .yaml/.yml extension)
**FILE NAME EXAMPLES:**
- "ludus_child_domain.yaml"
- "badsectorlabs.ludus_vulhub.yaml"
- "synzack.ludus_sccm.yaml"
- "Sample-schema.yaml"
**TIPS:**
- Use \`ludus_list_role_collection_schemas\` first to see available files
- File names are case-sensitive and must include extension
- Mix and match roles and collections in file_names array
- Error messages will show available files if names don't match`
}
]
};
}
// Read schemas from YAML files
const homeDir = os.homedir();
const schemaDir = path.join(homeDir, '.ludus-mcp', 'schemas');
// Check if schemas directory exists
try {
await fs.access(schemaDir);
} catch {
return {
content: [
{
type: 'text',
text: `Error: Schemas directory not found at ${schemaDir}. Please ensure YAML schema files have been downloaded from GitHub during server initialization.`
}
],
isError: true
};
}
// Load YAML schemas (all or filtered)
const aggregatedSchema = await loadAllYamlSchemas(schemaDir, file_names);
// Return the complete aggregated schema
return {
content: [
{
type: 'text',
text: JSON.stringify(aggregatedSchema, null, 2)
}
]
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error reading role schemas: ${error.message}\n\nTroubleshooting:\n- Restart the MCP server to reinitialize schemas\n- Check that YAML files exist in ~/.ludus-mcp/schemas/\n- Verify YAML file syntax is valid\n- Ensure file system permissions allow reading\n- Use exact file names with extensions (e.g., "ludus_ad.yaml")\n- Use ludus_list_role_collection_schemas to see available files`
}
],
isError: true
};
}
}