import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import { Logger } from '../utils/logger.js';
export async function handleLudusListRoleCollectionSchemas(args: any, logger: Logger): Promise<CallToolResult> {
try {
const { help = false } = args;
if (help) {
return {
content: [
{
type: 'text',
text: `**LUDUS LIST ROLE COLLECTION SCHEMAS TOOL**
**PURPOSE:**
Lists all available Ludus role and collection schema files in ~/.ludus-mcp/schemas/
**WHAT IT RETURNS:**
- Complete inventory of all schema files (.yaml and .yml)
- File sizes and basic metadata
- Separation of roles vs collections
- File status and accessibility
**FILE STRUCTURE:**
~/.ludus-mcp/schemas/
├── badsectorlabs.ludus_vulhub.yaml
├── synzack.ludus_sccm.yaml (collection)
├── ludus_child_domain.yaml
└── Sample-schema.yaml (template)
**USAGE:**
- Use before reading specific schemas to see what's available
- Get overview of role/collection inventory
- Verify schema files have been downloaded properly
- Find specific roles by name pattern
**OUTPUT:**
Lists all .yaml and .yml files with file information, separated by type (roles, collections, templates).`
}
]
};
}
// 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**
Schemas directory not found at: ${schemaDir}
**To Fix:**
1. Restart the MCP server to trigger schema download from GitHub
2. Check your internet connection
3. Verify the ~/.ludus-mcp/ directory exists
**Expected Location:** ~/.ludus-mcp/schemas/`
}
],
isError: true
};
}
// Get all YAML/YML files
const schemaFiles = await findYamlSchemaFiles(schemaDir);
if (schemaFiles.length === 0) {
return {
content: [
{
type: 'text',
text: `**No Schema Files Found**
Directory exists but no .yaml or .yml files found in: ${schemaDir}
**To Fix:**
1. Restart the MCP server to download schemas from GitHub
2. Check that the repository and schemas are available on main branch
3. Verify your internet connection
**Expected Files:** Role and collection schema files with .yaml or .yml extensions`
}
]
};
}
// Process files and categorize
const roles: SchemaFileInfo[] = [];
const collections: SchemaFileInfo[] = [];
const templates: SchemaFileInfo[] = [];
const unknown: SchemaFileInfo[] = [];
for (const filePath of schemaFiles) {
try {
const stats = await fs.stat(filePath);
const fileName = path.basename(filePath);
const fileSize = formatFileSize(stats.size);
// Read first few lines to determine type
const content = await fs.readFile(filePath, 'utf-8');
const lines = content.split('\n').slice(0, 10);
let type = 'unknown';
let name = fileName.replace(/\.(yaml|yml)$/, '');
// Parse type and name from YAML
for (const line of lines) {
if (line.startsWith('name:')) {
name = line.replace('name:', '').trim();
}
if (line.startsWith('type:')) {
type = line.replace('type:', '').trim().replace(/['"#]/g, '').split(' ')[0];
}
}
const fileInfo: SchemaFileInfo = {
fileName,
name,
type,
size: fileSize,
sizeBytes: stats.size,
lastModified: stats.mtime.toISOString().split('T')[0],
path: filePath
};
// Categorize files
if (fileName.includes('Sample-schema') || fileName.includes('template')) {
templates.push(fileInfo);
} else if (type === 'collection') {
collections.push(fileInfo);
} else if (type === 'role') {
roles.push(fileInfo);
} else {
unknown.push(fileInfo);
}
} catch (error) {
logger.warn('Failed to process schema file', { filePath, error: error instanceof Error ? error.message : String(error) });
}
}
// Sort arrays by name
roles.sort((a, b) => a.name.localeCompare(b.name));
collections.sort((a, b) => a.name.localeCompare(b.name));
templates.sort((a, b) => a.name.localeCompare(b.name));
unknown.sort((a, b) => a.name.localeCompare(b.name));
// Generate summary report
const totalFiles = roles.length + collections.length + templates.length + unknown.length;
const totalSize = [...roles, ...collections, ...templates, ...unknown]
.reduce((sum, file) => sum + file.sizeBytes, 0);
let report = `**LUDUS ROLE/COLLECTION SCHEMAS INVENTORY**
**📊 Summary:**
- **Total Files:** ${totalFiles}
- **Total Size:** ${formatFileSize(totalSize)}
- **Location:** ${schemaDir}
- **Last Scan:** ${new Date().toISOString().split('T')[0]}
`;
if (roles.length > 0) {
report += `**🎯 Individual Roles (${roles.length}):**
`;
for (const role of roles) {
report += `- **${role.name}** (${role.size}) - ${role.fileName}\n`;
}
report += '\n';
}
if (collections.length > 0) {
report += `**📦 Collections (${collections.length}):**
`;
for (const collection of collections) {
report += `- **${collection.name}** (${collection.size}) - ${collection.fileName}\n`;
}
report += '\n';
}
if (templates.length > 0) {
report += `**📋 Templates (${templates.length}):**
`;
for (const template of templates) {
report += `- **${template.name}** (${template.size}) - ${template.fileName}\n`;
}
report += '\n';
}
if (unknown.length > 0) {
report += `**❓ Unknown Type (${unknown.length}):**
`;
for (const file of unknown) {
report += `- **${file.name}** (${file.size}) - ${file.fileName}\n`;
}
report += '\n';
}
report += `**🔧 Next Steps:**
- Use \`ludus_read_role_collection_schema\` to read all schema data
- Use \`ludus_read_role_collection_schema\` with help=true for detailed usage
- Individual files are also available for direct reading if needed
**📁 File Extensions:** Both .yaml and .yml files are supported`;
return {
content: [
{
type: 'text',
text: report
}
]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Failed to list role collection schemas', { error: errorMessage });
return {
content: [
{
type: 'text',
text: `**Error listing role collection schemas**
${errorMessage}
**Troubleshooting:**
1. Check that ~/.ludus-mcp/schemas/ directory exists
2. Restart MCP server to download schemas
3. Verify internet connection for GitHub downloads`
}
],
isError: true
};
}
}
/**
* Find all YAML schema files (.yaml and .yml) in a directory
*/
async function findYamlSchemaFiles(dir: string): Promise<string[]> {
const files: string[] = [];
try {
const items = await fs.readdir(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
if (item.isFile() && (item.name.endsWith('.yaml') || item.name.endsWith('.yml'))) {
files.push(fullPath);
}
}
} catch (error) {
// Directory doesn't exist or can't be read
}
return files.sort();
}
/**
* Format file size in human readable format
*/
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
/**
* Schema file information interface
*/
interface SchemaFileInfo {
fileName: string;
name: string;
type: string;
size: string;
sizeBytes: number;
lastModified: string;
path: string;
}