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
}