UNPKG

@sofianedjerbi/knowledge-tree-mcp

Version:

MCP server for hierarchical project knowledge management

320 lines 10.7 kB
/** * Markdown parser for Knowledge Tree MCP * Converts between Markdown and KnowledgeEntry format */ import { isValidPriority, isValidRelationshipType } from '../../constants/index.js'; /** * Parses a Markdown file into a KnowledgeEntry */ export function parseMarkdownToEntry(content) { const errors = []; try { // Extract front matter const frontMatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!frontMatterMatch) { errors.push('Missing front matter. Add --- at start and end'); return { errors }; } const frontMatterText = frontMatterMatch[1]; const bodyText = frontMatterMatch[2]; // Parse front matter const frontMatter = parseFrontMatter(frontMatterText); // Validate required fields if (!frontMatter.title) { errors.push('Missing: title: Your Entry Title'); } if (!frontMatter.priority) { errors.push('Missing: priority: CRITICAL|REQUIRED|COMMON|EDGE-CASE'); } else if (!isValidPriority(frontMatter.priority)) { errors.push(`Invalid priority: "${frontMatter.priority}". Use: CRITICAL|REQUIRED|COMMON|EDGE-CASE`); } // Parse body sections const sections = parseBodySections(bodyText); // Validate required sections if (!sections.problem || sections.problem.trim() === '') { errors.push('Missing # Problem section with content'); } if (!sections.solution || sections.solution.trim() === '') { errors.push('Missing # Solution section with content'); } // Validate related entries format if (sections.related) { for (const rel of sections.related) { if (!rel.path.match(/^[\w\-\/]+(\.\w+)?$/)) { errors.push(`Invalid path format: "${rel.path}". Use: folder/subfolder/name.json`); } } } // Validate tags format if (frontMatter.tags && !Array.isArray(frontMatter.tags)) { if (!frontMatter.tags.match(/^\[.*\]$/)) { errors.push(`Invalid tags format. Use: tags: [tag1, tag2] or tags: tag1, tag2`); } } if (errors.length > 0) { return { errors }; } // Build entry const entry = { title: frontMatter.title, priority: frontMatter.priority, problem: sections.problem || '', solution: sections.solution || '' }; // Add optional fields if (frontMatter.slug) entry.slug = frontMatter.slug; if (frontMatter.category) entry.category = frontMatter.category; if (frontMatter.tags) { entry.tags = Array.isArray(frontMatter.tags) ? frontMatter.tags : frontMatter.tags.split(',').map(t => t.trim()); } if (sections.context) entry.context = sections.context; if (sections.examples && sections.examples.length > 0) { entry.examples = sections.examples; } if (frontMatter.author) entry.author = frontMatter.author; if (frontMatter.version) entry.version = frontMatter.version; if (frontMatter.created_at) entry.created_at = frontMatter.created_at; if (frontMatter.updated_at) entry.updated_at = frontMatter.updated_at; if (sections.related && sections.related.length > 0) { entry.related_to = sections.related; } return entry; } catch (error) { console.error('Failed to parse Markdown:', error); errors.push(`Parse error: ${error instanceof Error ? error.message : 'Unknown error'}`); return { errors }; } } /** * Converts a KnowledgeEntry to Markdown format */ export function convertEntryToMarkdown(entry) { const lines = []; // Front matter lines.push('---'); lines.push(`title: ${entry.title}`); lines.push(`priority: ${entry.priority}`); if (entry.slug) lines.push(`slug: ${entry.slug}`); if (entry.category) lines.push(`category: ${entry.category}`); if (entry.tags && entry.tags.length > 0) { lines.push(`tags: [${entry.tags.join(', ')}]`); } if (entry.author) lines.push(`author: ${entry.author}`); if (entry.version) lines.push(`version: ${entry.version}`); if (entry.created_at) lines.push(`created_at: ${entry.created_at}`); if (entry.updated_at) lines.push(`updated_at: ${entry.updated_at}`); lines.push('---'); lines.push(''); // Problem section lines.push('# Problem'); lines.push(''); lines.push(entry.problem); lines.push(''); // Context section (if present) if (entry.context) { lines.push('# Context'); lines.push(''); lines.push(entry.context); lines.push(''); } // Solution section lines.push('# Solution'); lines.push(''); lines.push(entry.solution); lines.push(''); // Examples section (if present) if (entry.examples && entry.examples.length > 0) { lines.push('# Examples'); lines.push(''); for (const example of entry.examples) { if (example.title) { lines.push(`## ${example.title}`); } if (example.description) { lines.push(`*${example.description}*`); lines.push(''); } if (example.code) { const lang = example.language || ''; lines.push(`\`\`\`${lang}`); lines.push(example.code); lines.push('```'); lines.push(''); } } } // Related section (if present) if (entry.related_to && entry.related_to.length > 0) { lines.push('# Related'); lines.push(''); for (const link of entry.related_to) { let line = `- ${link.relationship}: ${link.path}`; if (link.description) { line += ` - ${link.description}`; } lines.push(line); } lines.push(''); } return lines.join('\n'); } /** * Parse front matter YAML-like format */ function parseFrontMatter(text) { const frontMatter = {}; const lines = text.split('\n'); for (const line of lines) { const match = line.match(/^(\w+):\s*(.+)$/); if (match) { const [, key, value] = match; // Handle array values if (value.startsWith('[') && value.endsWith(']')) { const items = value.slice(1, -1).split(',').map(s => s.trim()); frontMatter[key] = items; } else { frontMatter[key] = value.trim(); } } } return frontMatter; } /** * Parse body sections from Markdown */ function parseBodySections(text) { const sections = {}; // Split by level 1 headers const parts = text.split(/^# /m).filter(Boolean); for (const part of parts) { const lines = part.trim().split('\n'); const header = lines[0].toLowerCase(); const content = lines.slice(1).join('\n').trim(); switch (header) { case 'problem': sections.problem = content; break; case 'context': sections.context = content; break; case 'solution': sections.solution = content; break; case 'examples': sections.examples = parseExamples(content); break; case 'related': sections.related = parseRelated(content); break; } } return sections; } /** * Parse examples section */ function parseExamples(content) { const examples = []; // Split by level 2 headers const parts = content.split(/^## /m).filter(Boolean); for (const part of parts) { const lines = part.trim().split('\n'); const title = lines[0].trim(); let description = ''; let code = ''; let language = ''; // Parse content let inCode = false; let codeLines = []; for (let i = 1; i < lines.length; i++) { const line = lines[i]; if (line.startsWith('```')) { if (!inCode) { inCode = true; language = line.slice(3).trim(); } else { code = codeLines.join('\n'); codeLines = []; inCode = false; } } else if (inCode) { codeLines.push(line); } else if (line.startsWith('*') && line.endsWith('*')) { description = line.slice(1, -1).trim(); } } const example = {}; if (title) example.title = title; if (description) example.description = description; if (code) example.code = code; if (language) example.language = language; if (Object.keys(example).length > 0) { examples.push(example); } } return examples; } /** * Parse related section */ function parseRelated(content) { const related = []; const lines = content.split('\n').filter(line => line.trim().startsWith('-')); for (const line of lines) { // Format: - relationship: path - description // Need to handle paths that may contain special chars const match = line.match(/^-\s*(\w+):\s*([^\s]+)(?:\s*-\s*(.+))?$/); if (match) { const [, relationship, path, description] = match; if (isValidRelationshipType(relationship)) { const relation = { path: path.trim(), relationship: relationship }; if (description) { relation.description = description.trim(); } related.push(relation); } } } return related; } /** * Check if a file path is markdown */ export function isMarkdownFile(path) { return path.endsWith('.md') || path.endsWith('.markdown'); } /** * Get the appropriate extension based on format preference */ export function getKnowledgeExtension(preferMarkdown = false) { return preferMarkdown ? '.md' : '.json'; } //# sourceMappingURL=parser.js.map