Skip to main content
Glama

Markdown MCP Server

by huntkil
MarkdownManager.js11 kB
import { promises as fs } from 'fs'; import { join, dirname, resolve } from 'path'; import { glob } from 'glob'; import matter from 'gray-matter'; import logger from './logger.js'; export class MarkdownManager { constructor(basePath = process.cwd()) { this.basePath = resolve(basePath); } /** * 파일 읽기 * @param {string} filePath - 파일 경로 * @param {string} encoding - 인코딩 (기본값: utf8) * @returns {Promise<string>} 파일 내용 */ async readFile(filePath, encoding = 'utf8') { const fullPath = this.getFullPath(filePath); this.validatePath(fullPath); try { return await fs.readFile(fullPath, encoding); } catch (error) { throw new Error(`Failed to read file: ${error.message}`); } } /** * 파일 생성 * @param {string} filePath - 파일 경로 * @param {string} content - 파일 내용 * @param {boolean} overwrite - 덮어쓰기 여부 * @returns {Promise<boolean>} 성공 여부 */ async createFile(filePath, content, overwrite = false) { const fullPath = this.getFullPath(filePath); this.validatePath(fullPath); try { if (!overwrite && await this.fileExists(fullPath)) { throw new Error('File already exists'); } await fs.mkdir(dirname(fullPath), { recursive: true }); await fs.writeFile(fullPath, content, 'utf8'); return true; } catch (error) { throw new Error(`Failed to create file: ${error.message}`); } } /** * 파일 수정 * @param {string} filePath - 파일 경로 * @param {string} content - 새로운 내용 * @param {boolean} append - 추가 여부 * @returns {Promise<boolean>} 성공 여부 */ async updateFile(filePath, content, append = false) { const fullPath = this.getFullPath(filePath); this.validatePath(fullPath); try { if (append) { const existingContent = await this.readFile(filePath); content = existingContent + '\n' + content; } await fs.writeFile(fullPath, content, 'utf8'); return true; } catch (error) { throw new Error(`Failed to update file: ${error.message}`); } } /** * 파일 삭제 * @param {string} filePath - 파일 경로 * @returns {Promise<boolean>} 성공 여부 */ async deleteFile(filePath) { const fullPath = this.getFullPath(filePath); this.validatePath(fullPath); try { await fs.unlink(fullPath); return true; } catch (error) { throw new Error(`Failed to delete file: ${error.message}`); } } /** * 파일 목록 조회 * @param {string} directory - 디렉토리 경로 * @param {boolean} recursive - 재귀 검색 여부 * @param {string} pattern - 파일 패턴 * @returns {Promise<string[]>} 파일 목록 */ async listFiles(directory, recursive = false, pattern = '*.md') { const searchPath = this.getFullPath(directory); this.validatePath(searchPath); try { const globPattern = recursive ? join(searchPath, '**', pattern) : join(searchPath, pattern); const files = await glob(globPattern); return files.map(file => file.replace(this.basePath, '').replace(/^\//, '')); } catch (error) { throw new Error(`Failed to list files: ${error.message}`); } } /** * 내용 검색 (확장 버전) * @param {string} directory - 검색할 디렉토리 * @param {Object} options - 검색 옵션 * @param {string} options.query - 단일 검색 키워드 (keywords와 함께 사용 불가) * @param {string[]} options.keywords - 여러 검색 키워드 배열 * @param {string} options.mode - 'and' 또는 'or' (기본값: 'or') * @param {boolean} options.caseSensitive - 대소문자 구분 여부 (기본값: false) * @param {boolean} options.isRegex - query를 정규식으로 처리할지 여부 (기본값: false) * @param {string} options.filenamePattern - 파일명 패턴 (정규식 또는 와일드카드) * @returns {Promise<Array>} 검색 결과 */ async searchContent(directory, options = {}) { const { query, keywords = [], mode = 'or', caseSensitive = false, isRegex = false, filenamePattern } = options; // query와 keywords는 동시에 사용할 수 없음 if (query && keywords.length > 0) { throw new Error('Cannot use both query and keywords simultaneously'); } // query나 keywords 중 하나는 반드시 필요 if (!query && keywords.length === 0) { throw new Error('Either query or keywords must be provided'); } let files = await this.listFiles(directory, true); // 파일명 패턴 필터링 if (filenamePattern) { try { // 와일드카드 패턴을 정규식으로 변환 const regexPattern = filenamePattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\?/g, '.'); const fileRegex = new RegExp(regexPattern, caseSensitive ? '' : 'i'); files = files.filter(file => fileRegex.test(file)); } catch (error) { throw new Error(`Invalid filename pattern: ${error.message}`); } } const results = []; for (const file of files) { try { const content = await this.readFile(file); const lines = content.split('\n'); lines.forEach((line, index) => { let match = false; if (keywords.length > 0) { // 여러 키워드 검색 if (mode === 'and') { // 모든 키워드가 포함되어야 함 match = keywords.every(keyword => { const regex = new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), caseSensitive ? 'g' : 'gi'); return regex.test(line); }); } else { // 하나라도 포함되면 됨 match = keywords.some(keyword => { const regex = new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), caseSensitive ? 'g' : 'gi'); return regex.test(line); }); } } else if (query) { // 단일 키워드/정규식 검색 try { let regex; if (isRegex) { regex = new RegExp(query, caseSensitive ? 'g' : 'gi'); } else { // 일반 문자열을 정규식으로 이스케이프 const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); regex = new RegExp(escapedQuery, caseSensitive ? 'g' : 'gi'); } match = regex.test(line); } catch (error) { logger.error(`Invalid regex pattern: ${query}`, error.message); return; // 이 라인은 건너뛰기 } } if (match) { results.push({ file, line: index + 1, content: line.trim(), match: line.match(new RegExp(query || keywords.join('|'), caseSensitive ? 'g' : 'gi')) }); } }); } catch (error) { logger.error(`Error searching in ${file}:`, error.message); } } return results; } /** * Frontmatter 관리 * @param {string} filePath - 파일 경로 * @param {string} action - 액션 (get, set, update, remove) * @param {Object} metadata - 메타데이터 * @returns {Promise<Object>} 작업 결과 */ async manageFrontmatter(filePath, action, metadata = null) { const fullPath = this.getFullPath(filePath); this.validatePath(fullPath); try { const fileContent = await this.readFile(filePath); const { data, content } = matter(fileContent); switch (action) { case 'get': { return { metadata: data, success: true }; } case 'set': { if (!metadata) throw new Error('Metadata is required for set operation'); const newContent = matter.stringify(content, metadata); await this.updateFile(filePath, newContent); return { metadata, success: true }; } case 'update': { if (!metadata) throw new Error('Metadata is required for update operation'); const updatedData = { ...data, ...metadata }; const updatedContent = matter.stringify(content, updatedData); await this.updateFile(filePath, updatedContent); return { metadata: updatedData, success: true }; } case 'remove': { await this.updateFile(filePath, content); return { success: true }; } default: throw new Error(`Unknown action: ${action}`); } } catch (error) { throw new Error(`Failed to manage frontmatter: ${error.message}`); } } // 유틸리티 메서드들 getFullPath(filePath) { return resolve(this.basePath, filePath); } validatePath(fullPath) { if (!fullPath.startsWith(this.basePath)) { throw new Error('Invalid file path: outside base directory'); } } async fileExists(filePath) { try { await fs.access(filePath); return true; } catch { return false; } } }

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/huntkil/mcp_js'

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