Skip to main content
Glama

Memory MCP Server

by inchan
front-matter.ts10.3 kB
/** * Front Matter 처리 모듈 */ import matter from 'gray-matter'; import { z } from 'zod'; import { FrontMatterSchema, FrontMatter, MarkdownNote, generateUid, logger } from '@memory-mcp/common'; import { FrontMatterValidationError, UpdateFrontMatterOptions, StorageMdError } from './types'; /** * Front Matter 파싱 결과 */ interface ParseResult { frontMatter: FrontMatter; content: string; isEmpty: boolean; } /** * Front Matter 기본값 생성 */ function createDefaultFrontMatter( title: string, category: FrontMatter['category'] = 'Resources' ): Partial<FrontMatter> { const now = new Date().toISOString(); return { id: generateUid(), title: title || 'Untitled', category, tags: [], created: now, updated: now, links: [], }; } /** * Zod 오류를 사용자 친화적 메시지로 변환 */ function formatValidationErrors(error: z.ZodError): string[] { return error.errors.map(err => { const path = err.path.join('.'); return `${path}: ${err.message}`; }); } /** * Markdown 파일의 Front Matter 파싱 */ export function parseFrontMatter( fileContent: string, filePath?: string, strict: boolean = true ): ParseResult { try { // gray-matter로 파싱 const parsed = matter(fileContent); const contentBody = parsed.content.startsWith('\r\n') ? parsed.content.slice(2) : parsed.content.startsWith('\n') ? parsed.content.slice(1) : parsed.content; // Front Matter가 비어있는 경우 if (!parsed.data || Object.keys(parsed.data).length === 0) { logger.debug(`Front Matter가 비어있음: ${filePath || 'unknown'}`); if (strict) { throw new FrontMatterValidationError( 'Front Matter가 없거나 비어있습니다', filePath ); } return { frontMatter: createDefaultFrontMatter('Untitled') as FrontMatter, content: contentBody, isEmpty: true, }; } // Zod 스키마 검증 const validationResult = FrontMatterSchema.safeParse(parsed.data); if (!validationResult.success) { const errors = formatValidationErrors(validationResult.error); logger.warn(`Front Matter 검증 실패: ${filePath || 'unknown'}`, errors); if (strict) { throw new FrontMatterValidationError( `Front Matter 검증 실패: ${errors.join(', ')}`, filePath, errors ); } // 비엄격 모드에서는 기본값과 병합 const defaultFM = createDefaultFrontMatter( parsed.data.title || 'Untitled', parsed.data.category ); const mergedFM = { ...defaultFM, ...parsed.data }; const finalValidation = FrontMatterSchema.safeParse(mergedFM); if (!finalValidation.success) { // 여전히 실패하면 완전히 기본값 사용 return { frontMatter: createDefaultFrontMatter('Untitled') as FrontMatter, content: contentBody, isEmpty: false, }; } return { frontMatter: finalValidation.data, content: contentBody, isEmpty: false, }; } return { frontMatter: validationResult.data, content: contentBody, isEmpty: false, }; } catch (error) { if (error instanceof FrontMatterValidationError) { throw error; } throw new StorageMdError( `Front Matter 파싱 실패: ${error instanceof Error ? error.message : 'Unknown error'}`, 'FRONT_MATTER_PARSE_ERROR', filePath, error ); } } /** * Front Matter를 YAML 문자열로 직렬화 */ export function serializeFrontMatter(frontMatter: FrontMatter): string { try { // Zod 스키마로 재검증 (안전성) const validated = FrontMatterSchema.parse(frontMatter); // gray-matter를 사용하여 YAML로 직렬화 const serialized = matter.stringify('', validated); // Front Matter 부분만 추출 (--- 포함) const frontMatterMatch = serialized.match(/^---\n([\s\S]*?)\n---/); if (!frontMatterMatch) { throw new Error('Front Matter 직렬화 결과가 예상과 다릅니다'); } return frontMatterMatch[0]; } catch (error) { throw new StorageMdError( `Front Matter 직렬화 실패: ${error instanceof Error ? error.message : 'Unknown error'}`, 'FRONT_MATTER_SERIALIZE_ERROR', undefined, error ); } } /** * 전체 Markdown 노트를 문자열로 직렬화 */ export function serializeMarkdownNote(note: MarkdownNote): string { try { const frontMatterYaml = serializeFrontMatter(note.frontMatter); return `${frontMatterYaml}\n\n${note.content}`; } catch (error) { throw new StorageMdError( `Markdown 노트 직렬화 실패: ${error instanceof Error ? error.message : 'Unknown error'}`, 'MARKDOWN_SERIALIZE_ERROR', note.filePath, error ); } } /** * Front Matter 부분 업데이트 */ export function updateFrontMatter( currentFrontMatter: FrontMatter, updates: Partial<FrontMatter>, options: UpdateFrontMatterOptions = {} ): FrontMatter { const { updateTimestamp = true, allowPartial = true, } = options; try { // 업데이트 적용 let updatedFrontMatter = { ...currentFrontMatter, ...updates }; // 타임스탬프 자동 갱신 if (updateTimestamp) { updatedFrontMatter.updated = new Date().toISOString(); } // 스키마 검증 const validationResult = FrontMatterSchema.safeParse(updatedFrontMatter); if (!validationResult.success) { const errors = formatValidationErrors(validationResult.error); if (!allowPartial) { throw new FrontMatterValidationError( `Front Matter 업데이트 검증 실패: ${errors.join(', ')}`, undefined, errors ); } // 부분 업데이트 허용 - 실패한 필드는 원래 값 유지 logger.warn('Front Matter 업데이트 중 일부 검증 실패, 원래 값 유지', errors); for (const error of validationResult.error.errors) { const fieldPath = error.path.join('.'); logger.debug(`검증 실패 필드 복원: ${fieldPath}`); // 중첩 객체 처리는 간단히 최상위 필드만 if (error.path.length === 1) { const field = error.path[0] as keyof FrontMatter; if (field in currentFrontMatter) { (updatedFrontMatter as any)[field] = currentFrontMatter[field]; } } } // 재검증 const revalidated = FrontMatterSchema.safeParse(updatedFrontMatter); if (!revalidated.success) { // 여전히 실패하면 전체 롤백 logger.error('Front Matter 업데이트 후 재검증 실패, 전체 롤백'); return currentFrontMatter; } updatedFrontMatter = revalidated.data; } else { updatedFrontMatter = validationResult.data; } logger.debug('Front Matter 업데이트 완료', { id: updatedFrontMatter.id, title: updatedFrontMatter.title }); return updatedFrontMatter; } catch (error) { if (error instanceof FrontMatterValidationError) { throw error; } throw new StorageMdError( `Front Matter 업데이트 실패: ${error instanceof Error ? error.message : 'Unknown error'}`, 'FRONT_MATTER_UPDATE_ERROR', undefined, error ); } } /** * Front Matter에 링크 추가 */ export function addLinkToFrontMatter( frontMatter: FrontMatter, linkUid: string ): FrontMatter { const currentLinks = frontMatter.links || []; // 중복 방지 if (currentLinks.includes(linkUid)) { return frontMatter; } return updateFrontMatter(frontMatter, { links: [...currentLinks, linkUid] }); } /** * Front Matter에서 링크 제거 */ export function removeLinkFromFrontMatter( frontMatter: FrontMatter, linkUid: string ): FrontMatter { const currentLinks = frontMatter.links || []; const updatedLinks = currentLinks.filter(uid => uid !== linkUid); return updateFrontMatter(frontMatter, { links: updatedLinks }); } /** * Front Matter에 태그 추가 */ export function addTagToFrontMatter( frontMatter: FrontMatter, tag: string ): FrontMatter { const currentTags = frontMatter.tags || []; // 중복 방지 if (currentTags.includes(tag)) { return frontMatter; } return updateFrontMatter(frontMatter, { tags: [...currentTags, tag] }); } /** * Front Matter에서 태그 제거 */ export function removeTagFromFrontMatter( frontMatter: FrontMatter, tag: string ): FrontMatter { const currentTags = frontMatter.tags || []; const updatedTags = currentTags.filter(t => t !== tag); return updateFrontMatter(frontMatter, { tags: updatedTags }); } /** * 제목에서 자동으로 기본 Front Matter 생성 */ export function generateFrontMatterFromTitle( title: string, category: FrontMatter['category'] = 'Resources', additionalData?: Partial<FrontMatter> ): FrontMatter { const defaultFM = createDefaultFrontMatter(title, category); const merged = { ...defaultFM, ...additionalData }; // 검증 const validated = FrontMatterSchema.parse(merged); return validated; } /** * Front Matter 필드 유효성 개별 검증 */ export function validateFrontMatterField( field: keyof FrontMatter, value: any ): boolean { try { const fieldSchema = FrontMatterSchema.shape[field]; fieldSchema.parse(value); return true; } catch { return false; } } /** * Front Matter 깊은 비교 */ export function frontMatterEquals( fm1: FrontMatter, fm2: FrontMatter ): boolean { try { return JSON.stringify(fm1) === JSON.stringify(fm2); } catch { return false; } } /** * Front Matter 요약 정보 생성 (로깅용) */ export function createFrontMatterSummary(frontMatter: FrontMatter): Record<string, any> { return { id: frontMatter.id, title: frontMatter.title, category: frontMatter.category, tagCount: frontMatter.tags?.length || 0, linkCount: frontMatter.links?.length || 0, project: frontMatter.project, created: frontMatter.created, updated: frontMatter.updated, }; }

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/inchan/memory-mcp'

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