Skip to main content
Glama
document-mcp-server.js21.7 kB
/** * WorkflowMCP Document Management Server - Phase 2.7 * SQLite 기반 문서 관리 전용 MCP 서버 * 프로젝트 문서를 체계적으로 저장, 검색, 관리 */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import sqlite3 from 'sqlite3'; import { open } from 'sqlite'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Database path const DB_PATH = path.resolve(__dirname, '../data/workflow.db'); let db; /** * 서버 초기화 */ async function initializeServer() { console.log('📚 Initializing Document Management MCP Server...'); try { db = await open({ filename: DB_PATH, driver: sqlite3.Database }); await db.exec('PRAGMA foreign_keys = ON'); // Check if document tables exist const tables = await db.all(` SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%document%' `); if (tables.length === 0) { console.log('⚠️ Document tables not found. Please run migration first:'); console.log(' node src/database/migrate-documents.js'); process.exit(1); } console.log('✅ Document Management database ready'); return true; } catch (error) { console.error('❌ Database initialization failed:', error); throw error; } } // MCP 서버 생성 const server = new Server({ name: 'workflow-document-mcp', version: '2.7.0' }); // ============================================= // MCP 도구 목록 정의 // ============================================= server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // 문서 생성 및 관리 { name: 'create_document', description: '새 문서를 생성하고 SQLite에 저장', inputSchema: { type: 'object', properties: { title: { type: 'string', description: '문서 제목' }, content: { type: 'string', description: '문서 내용 (Markdown 형식)' }, doc_type: { type: 'string', enum: ['test_guide', 'test_results', 'analysis', 'report', 'checklist', 'specification', 'meeting_notes', 'decision_log'], description: '문서 유형' }, category: { type: 'string', description: '카테고리 (예: phase_2.6, testing)' }, tags: { type: 'array', items: { type: 'string' }, description: '태그 목록' }, summary: { type: 'string', description: '문서 요약' }, linked_entity_type: { type: 'string', enum: ['prd', 'task', 'plan'], description: '연결할 엔터티 유형 (선택사항)' }, linked_entity_id: { type: 'string', description: '연결할 엔터티 ID (선택사항)' }, link_type: { type: 'string', enum: ['specification', 'test_plan', 'result', 'analysis', 'notes'], description: '링크 유형 (선택사항)' } }, required: ['title', 'content', 'doc_type'] } }, { name: 'search_documents', description: 'Full-text search로 문서 검색', inputSchema: { type: 'object', properties: { query: { type: 'string', description: '검색 쿼리' }, doc_type: { type: 'string', enum: ['test_guide', 'test_results', 'analysis', 'report', 'checklist', 'specification', 'meeting_notes', 'decision_log'], description: '특정 문서 유형으로 필터 (선택사항)' }, category: { type: 'string', description: '특정 카테고리로 필터 (선택사항)' }, limit: { type: 'integer', default: 10, description: '결과 제한 수' } }, required: ['query'] } }, { name: 'get_document', description: 'ID로 특정 문서 조회', inputSchema: { type: 'object', properties: { id: { type: 'integer', description: '문서 ID' } }, required: ['id'] } }, { name: 'update_document', description: '기존 문서 업데이트', inputSchema: { type: 'object', properties: { id: { type: 'integer', description: '문서 ID' }, title: { type: 'string', description: '문서 제목' }, content: { type: 'string', description: '문서 내용' }, summary: { type: 'string', description: '문서 요약' }, status: { type: 'string', enum: ['draft', 'review', 'approved', 'archived'], description: '문서 상태' }, tags: { type: 'array', items: { type: 'string' }, description: '태그 목록' } }, required: ['id'] } }, { name: 'delete_document', description: '문서 삭제', inputSchema: { type: 'object', properties: { id: { type: 'integer', description: '문서 ID' } }, required: ['id'] } }, { name: 'list_documents', description: '문서 목록 조회', inputSchema: { type: 'object', properties: { doc_type: { type: 'string', enum: ['test_guide', 'test_results', 'analysis', 'report', 'checklist', 'specification', 'meeting_notes', 'decision_log'], description: '특정 문서 유형으로 필터' }, category: { type: 'string', description: '특정 카테고리로 필터' }, status: { type: 'string', enum: ['draft', 'review', 'approved', 'archived'], description: '특정 상태로 필터' }, limit: { type: 'integer', default: 20, description: '결과 제한 수' } } } }, { name: 'import_markdown_file', description: '기존 Markdown 파일을 문서로 가져오기', inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: '가져올 Markdown 파일 경로' }, doc_type: { type: 'string', enum: ['test_guide', 'test_results', 'analysis', 'report', 'checklist', 'specification', 'meeting_notes', 'decision_log'], description: '문서 유형' }, category: { type: 'string', description: '카테고리' }, tags: { type: 'array', items: { type: 'string' }, description: '태그 목록' }, auto_summary: { type: 'boolean', default: true, description: '자동 요약 생성 여부' } }, required: ['file_path', 'doc_type'] } }, { name: 'link_document', description: '문서를 PRD, Task, Plan에 연결', inputSchema: { type: 'object', properties: { document_id: { type: 'integer', description: '문서 ID' }, entity_type: { type: 'string', enum: ['prd', 'task', 'plan'], description: '연결할 엔터티 유형' }, entity_id: { type: 'string', description: '연결할 엔터티 ID' }, link_type: { type: 'string', enum: ['specification', 'test_plan', 'result', 'analysis', 'notes'], default: 'notes', description: '링크 유형' } }, required: ['document_id', 'entity_type', 'entity_id'] } }, { name: 'get_document_links', description: '문서의 모든 연결 관계 조회', inputSchema: { type: 'object', properties: { document_id: { type: 'integer', description: '문서 ID' } }, required: ['document_id'] } } ] }; }); // ============================================= // 도구 핸들러 구현 // ============================================= server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'create_document': return await handleCreateDocument(args); case 'search_documents': return await handleSearchDocuments(args); case 'get_document': return await handleGetDocument(args); case 'update_document': return await handleUpdateDocument(args); case 'delete_document': return await handleDeleteDocument(args); case 'list_documents': return await handleListDocuments(args); case 'import_markdown_file': return await handleImportMarkdownFile(args); case 'link_document': return await handleLinkDocument(args); case 'get_document_links': return await handleGetDocumentLinks(args); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { console.error(`Error in tool ${name}:`, error); return { content: [{ type: 'text', text: `❌ Error: ${error.message}` }], isError: true }; } }); // ============================================= // 핸들러 함수들 // ============================================= async function handleCreateDocument(args) { const { title, content, doc_type, category, tags, summary, linked_entity_type, linked_entity_id, link_type } = args; // Insert document const insertDoc = db.prepare(` INSERT INTO documents (title, content, doc_type, category, tags, summary, created_by) VALUES (?, ?, ?, ?, ?, ?, 'mcp-user') `); const result = insertDoc.run( title, content, doc_type, category || null, tags ? JSON.stringify(tags) : null, summary || null ); const documentId = result.lastInsertRowid; // Link to entity if specified if (linked_entity_type && linked_entity_id) { const insertLink = db.prepare(` INSERT INTO document_links (document_id, linked_entity_type, linked_entity_id, link_type) VALUES (?, ?, ?, ?) `); insertLink.run(documentId, linked_entity_type, linked_entity_id, link_type || 'notes'); } return { content: [{ type: 'text', text: `✅ 문서가 생성되었습니다! **문서 ID**: ${documentId} **제목**: ${title} **유형**: ${doc_type} **카테고리**: ${category || '없음'} **태그**: ${tags ? tags.join(', ') : '없음'} ${linked_entity_type ? `**연결됨**: ${linked_entity_type} #${linked_entity_id} (${link_type || 'notes'})` : ''} 🔍 **검색 인덱싱**: 자동으로 전문 검색 인덱스에 추가됨` }] }; } async function handleSearchDocuments(args) { const { query, doc_type, category, limit = 10 } = args; let sql = ` SELECT d.id, d.title, d.doc_type, d.category, d.summary, d.created_at, snippet(documents_fts, 1, '<mark>', '</mark>', '...', 32) as snippet FROM documents d JOIN documents_fts ON d.id = documents_fts.rowid WHERE documents_fts MATCH ? `; const params = [query]; if (doc_type) { sql += ' AND d.doc_type = ?'; params.push(doc_type); } if (category) { sql += ' AND d.category = ?'; params.push(category); } sql += ' ORDER BY documents_fts.bm25 LIMIT ?'; params.push(limit); const results = db.prepare(sql).all(...params); if (results.length === 0) { return { content: [{ type: 'text', text: `🔍 검색 결과가 없습니다. **검색어**: "${query}" ${doc_type ? `**문서 유형**: ${doc_type}` : ''} ${category ? `**카테고리**: ${category}` : ''} 💡 **검색 팁**: - 다른 키워드 시도 - 필터 조건 완화 - 전체 문서 목록: \`list_documents\` 사용` }] }; } return { content: [{ type: 'text', text: `🔍 검색 결과 (${results.length}개) **검색어**: "${query}" ${doc_type ? `**필터**: ${doc_type}` : ''} ${category ? `**카테고리**: ${category}` : ''} ${results.map(doc => ` **[${doc.id}] ${doc.title}** 📋 유형: ${doc.doc_type} | 📂 카테고리: ${doc.category || '없음'} 📅 생성: ${new Date(doc.created_at).toLocaleDateString('ko-KR')} 📝 요약: ${doc.summary || '없음'} 🔍 발견: ${doc.snippet} ---`).join('\n')} 💡 특정 문서 보기: \`get_document\` 사용` }] }; } async function handleGetDocument(args) { const { id } = args; const doc = db.prepare(` SELECT d.*, GROUP_CONCAT( dl.linked_entity_type || ':' || dl.linked_entity_id || ':' || dl.link_type, '|' ) as links FROM documents d LEFT JOIN document_links dl ON d.id = dl.document_id WHERE d.id = ? GROUP BY d.id `).get(id); if (!doc) { return { content: [{ type: 'text', text: `❌ ID ${id}에 해당하는 문서를 찾을 수 없습니다.` }] }; } const tags = doc.tags ? JSON.parse(doc.tags) : []; const links = doc.links ? doc.links.split('|').map(link => { const [type, id, linkType] = link.split(':'); return { type, id, linkType }; }) : []; return { content: [{ type: 'text', text: `📄 **${doc.title}** **문서 ID**: ${doc.id} **유형**: ${doc.doc_type} **카테고리**: ${doc.category || '없음'} **상태**: ${doc.status} **태그**: ${tags.join(', ') || '없음'} **생성일**: ${new Date(doc.created_at).toLocaleString('ko-KR')} **수정일**: ${new Date(doc.updated_at).toLocaleString('ko-KR')} **작성자**: ${doc.created_by} **버전**: ${doc.version} ${doc.summary ? `**요약**: ${doc.summary}\n` : ''} ${links.length > 0 ? `**연결된 항목**:\n${links.map(link => `- ${link.type} #${link.id} (${link.linkType})`).join('\n')}\n` : ''} --- ${doc.content}` }] }; } async function handleUpdateDocument(args) { const { id, title, content, summary, status, tags } = args; // Check if document exists const existing = db.prepare('SELECT id FROM documents WHERE id = ?').get(id); if (!existing) { throw new Error(`Document ID ${id} not found`); } // Build update query dynamically const updates = []; const params = []; if (title !== undefined) { updates.push('title = ?'); params.push(title); } if (content !== undefined) { updates.push('content = ?'); params.push(content); } if (summary !== undefined) { updates.push('summary = ?'); params.push(summary); } if (status !== undefined) { updates.push('status = ?'); params.push(status); } if (tags !== undefined) { updates.push('tags = ?'); params.push(JSON.stringify(tags)); } updates.push('updated_at = CURRENT_TIMESTAMP'); updates.push('version = version + 1'); params.push(id); const sql = `UPDATE documents SET ${updates.join(', ')} WHERE id = ?`; db.prepare(sql).run(...params); return { content: [{ type: 'text', text: `✅ 문서 ID ${id}가 성공적으로 업데이트되었습니다. **업데이트된 필드**: ${title ? `- 제목: ${title}` : ''} ${content ? `- 내용: 업데이트됨` : ''} ${summary ? `- 요약: ${summary}` : ''} ${status ? `- 상태: ${status}` : ''} ${tags ? `- 태그: ${tags.join(', ')}` : ''} 🔄 **버전**: 자동 증가 📅 **수정 시간**: ${new Date().toLocaleString('ko-KR')}` }] }; } async function handleDeleteDocument(args) { const { id } = args; const doc = db.prepare('SELECT title FROM documents WHERE id = ?').get(id); if (!doc) { throw new Error(`Document ID ${id} not found`); } db.prepare('DELETE FROM documents WHERE id = ?').run(id); return { content: [{ type: 'text', text: `🗑️ 문서가 삭제되었습니다. **문서 ID**: ${id} **제목**: ${doc.title} ⚠️ **주의**: 연결된 링크와 관계도 함께 삭제되었습니다.` }] }; } async function handleListDocuments(args) { const { doc_type, category, status, limit = 20 } = args; let sql = 'SELECT * FROM document_overview WHERE 1=1'; const params = []; if (doc_type) { sql += ' AND doc_type = ?'; params.push(doc_type); } if (category) { sql += ' AND category = ?'; params.push(category); } if (status) { sql += ' AND status = ?'; params.push(status); } sql += ' ORDER BY updated_at DESC LIMIT ?'; params.push(limit); const docs = db.prepare(sql).all(...params); return { content: [{ type: 'text', text: `📚 문서 목록 (${docs.length}개) ${doc_type ? `📋 **필터 - 유형**: ${doc_type}` : ''} ${category ? `📂 **필터 - 카테고리**: ${category}` : ''} ${status ? `🏷️ **필터 - 상태**: ${status}` : ''} ${docs.map(doc => ` **[${doc.id}] ${doc.title}** 📋 ${doc.doc_type} | 📂 ${doc.category || '없음'} | 🏷️ ${doc.status} 📅 ${new Date(doc.updated_at).toLocaleDateString('ko-KR')} 🔗 연결: ${doc.linked_entities_count}개 | 📑 관련 문서: ${doc.related_docs_count}개 📝 ${doc.summary || '요약 없음'} ---`).join('\n')} 💡 **사용법**: - 문서 보기: \`get_document\` 사용 - 검색: \`search_documents\` 사용` }] }; } async function handleImportMarkdownFile(args) { const { file_path, doc_type, category, tags, auto_summary = true } = args; if (!fs.existsSync(file_path)) { throw new Error(`File not found: ${file_path}`); } const content = fs.readFileSync(file_path, 'utf8'); const title = path.basename(file_path, '.md').replace(/[-_]/g, ' '); // Auto-generate summary from first paragraph if enabled let summary = null; if (auto_summary) { const firstParagraph = content.split('\n\n')[0]; summary = firstParagraph.replace(/^#+\s*/, '').substring(0, 200); } const insertDoc = db.prepare(` INSERT INTO documents (title, content, doc_type, category, file_path, tags, summary, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, 'file-import') `); const result = insertDoc.run( title, content, doc_type, category || null, file_path, tags ? JSON.stringify(tags) : null, summary ); return { content: [{ type: 'text', text: `📁 파일을 문서로 가져왔습니다! **문서 ID**: ${result.lastInsertRowid} **파일**: ${file_path} **제목**: ${title} **유형**: ${doc_type} **크기**: ${content.length}자 **자동 요약**: ${auto_summary ? '생성됨' : '건너뜀'} ✅ 전문 검색 인덱스에 자동 추가되었습니다.` }] }; } async function handleLinkDocument(args) { const { document_id, entity_type, entity_id, link_type = 'notes' } = args; // Check if document exists const doc = db.prepare('SELECT title FROM documents WHERE id = ?').get(document_id); if (!doc) { throw new Error(`Document ID ${document_id} not found`); } // Insert link const insertLink = db.prepare(` INSERT OR IGNORE INTO document_links (document_id, linked_entity_type, linked_entity_id, link_type) VALUES (?, ?, ?, ?) `); const result = insertLink.run(document_id, entity_type, entity_id, link_type); return { content: [{ type: 'text', text: `🔗 연결이 생성되었습니다! **문서**: [${document_id}] ${doc.title} **연결 대상**: ${entity_type} #${entity_id} **링크 유형**: ${link_type} **상태**: ${result.changes > 0 ? '새로 생성됨' : '이미 존재함'} 💡 연결 확인: \`get_document_links\` 사용` }] }; } async function handleGetDocumentLinks(args) { const { document_id } = args; const doc = db.prepare('SELECT title FROM documents WHERE id = ?').get(document_id); if (!doc) { throw new Error(`Document ID ${document_id} not found`); } const links = db.prepare(` SELECT linked_entity_type, linked_entity_id, link_type, created_at FROM document_links WHERE document_id = ? ORDER BY created_at DESC `).all(document_id); return { content: [{ type: 'text', text: `🔗 문서 연결 관계 **문서**: [${document_id}] ${doc.title} **총 연결**: ${links.length}개 ${links.length > 0 ? links.map(link => ` - **${link.linked_entity_type} #${link.linked_entity_id}** 유형: ${link.link_type} 생성: ${new Date(link.created_at).toLocaleDateString('ko-KR')} `).join('\n') : '연결된 항목이 없습니다.' } 💡 새 연결 생성: \`link_document\` 사용` }] }; } // ============================================= // 서버 시작 // ============================================= async function main() { try { await initializeServer(); const transport = new StdioServerTransport(); await server.connect(transport); console.log('✅ Document Management MCP Server ready - 9 document tools available'); } catch (error) { console.error('💥 Server startup failed:', error); process.exit(1); } } // 종료 시 정리 process.on('SIGINT', async () => { console.log('\n🔄 Shutting down document server...'); if (db) { db.close(); } process.exit(0); }); 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/foswmine/workflow-mcp'

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