@sofianedjerbi/knowledge-tree-mcp
Version:
MCP server for hierarchical project knowledge management
320 lines • 10.7 kB
JavaScript
/**
* 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