Skip to main content
Glama

IBM i MCP Server

Official
by IBM
convert-rules-to-yaml.tsโ€ข5.9 kB
/** * Script: convert-rules-to-yaml.ts * Purpose: Convert legacy .rule files (title, description, sql, parameters?, post) into * a consolidated YAML tools file compatible with YamlToolsConfig schema. * * Usage: npx tsx scripts/convert-rules-to-yaml.ts [--out rules/security-tools.yaml] [--source ibmi-system] */ import fs from "fs"; import path from "path"; import yaml from "js-yaml"; interface LegacyRuleFile { title: string; description: string; sql: string; parameters?: { name: string; description?: string }[]; post?: string; } interface YamlToolParameterOut { name: string; type: "string"; description?: string; required?: boolean; } interface YamlToolOut { source: string; description: string; statement: string; parameters?: YamlToolParameterOut[]; domain?: string; category?: string; metadata?: Record<string, unknown>; } const RULES_DIR = path.resolve("rules"); function slugify(title: string): string { return title .toLowerCase() .replace(/database file/g, "dbfile") .replace(/[^a-z0-9]+/g, "_") .replace(/^_+|_+$/g, "") .replace(/__+/g, "_"); } function categorize(title: string): string { const t = title.toLowerCase(); if (t.includes("ifs home")) return "ifs"; if (t.includes("password failure")) return "password_failures"; if (t.includes("system value")) return "system_values"; if (t.includes("authority failure")) return "authority_failures"; if (t.includes("trigger")) return "triggers"; if (t.includes("rename")) return "rename"; if (t.includes("insert") || t.includes("update") || t.includes("delete")) return "data_privileges"; if (t.includes("user profile") || t.includes("user profiles")) return "user_profiles"; if (t.includes("library list")) return "library_list"; return "security_misc"; } function readRuleFile(filePath: string): LegacyRuleFile | undefined { const raw = fs.readFileSync(filePath, "utf8"); try { // Treat the .rule file as YAML directly const data = yaml.load(raw) as unknown; if (!data || typeof data !== "object") return undefined; const record = data as Record<string, unknown>; if (!record.sql) { console.warn(`Skipping ${path.basename(filePath)} - missing sql`); return undefined; } return { title: record.title?.toString() ?? path.basename(filePath, ".rule"), description: record.description?.toString() ?? "", sql: record.sql?.toString() ?? "", parameters: Array.isArray(record.parameters) ? (record.parameters as unknown[]) .map((p): { name: string; description?: string } | undefined => { if (p && typeof p === "object") { const param = p as Record<string, unknown>; const nameVal = param.name; if (typeof nameVal === "string" && nameVal.trim()) { const descVal = param.description; return { name: nameVal.trim(), description: typeof descVal === "string" ? descVal : undefined, }; } } return undefined; }) .filter((p): p is { name: string; description?: string } => !!p) : undefined, post: record.post?.toString(), }; } catch (err) { console.error(`Failed to parse ${filePath}:`, err); return undefined; } } function buildTool( rule: LegacyRuleFile, source: string, originalFile: string, ): [string, YamlToolOut] { const name = slugify(rule.title); const category = categorize(rule.title); const parameters: YamlToolParameterOut[] | undefined = rule.parameters?.map( (p) => ({ name: p.name, type: "string", description: p.description, }), ); // Merge post content into description metadata but keep original description succinct const metadata: Record<string, unknown> = { title: rule.title, original_file: path.basename(originalFile), }; if (rule.post) metadata.post = rule.post.trim(); // Keep main description just the original description (trim) const description = rule.description.trim(); return [ name, { source, description, statement: rule.sql.trim(), ...(parameters && parameters.length ? { parameters } : {}), domain: "security", category, metadata, }, ]; } function main() { const args = process.argv.slice(2); const outFlagIndex = args.indexOf("--out"); const sourceFlagIndex = args.indexOf("--source"); const outPath = outFlagIndex >= 0 ? args[outFlagIndex + 1] : "rules/security-tools.yaml"; const sourceName = sourceFlagIndex >= 0 ? args[sourceFlagIndex + 1] : "ibmi-system"; if (!fs.existsSync(RULES_DIR)) { console.error(`Rules directory not found: ${RULES_DIR}`); process.exit(1); } const files = fs.readdirSync(RULES_DIR).filter((f) => f.endsWith(".rule")); const tools: Record<string, YamlToolOut> = {}; for (const file of files) { const full = path.join(RULES_DIR, file); const rule = readRuleFile(full); if (!rule) continue; const [toolName, toolDef] = buildTool(rule, sourceName, full); if (tools[toolName]) { console.warn(`Duplicate tool name ${toolName}; appending hash suffix`); const altName = `${toolName}_${Math.random().toString(36).slice(2, 6)}`; tools[altName] = toolDef; } else { tools[toolName] = toolDef; } } const outAbs = path.resolve(outPath); const outDir = path.dirname(outAbs); fs.mkdirSync(outDir, { recursive: true }); const doc = { tools }; const yamlStr = yaml.dump(doc, { lineWidth: 100 }); fs.writeFileSync(outAbs, yamlStr, "utf8"); console.log( `Generated YAML with ${Object.keys(tools).length} tools at ${outAbs}`, ); } if (import.meta.url === `file://${process.argv[1]}`) { main(); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/IBM/ibmi-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server