UNPKG

@shirokuma-library/mcp-knowledge-base

Version:

MCP server for AI-powered knowledge management with semantic search, graph analysis, and automatic enrichment

436 lines (435 loc) 19.2 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { AppDataSource } from '../data-source.js'; import { ItemRepository } from '../repositories/ItemRepository.js'; import { SystemStateRepository } from '../repositories/SystemStateRepository.js'; import { Status } from '../entities/Status.js'; import { Tag } from '../entities/Tag.js'; import { ItemTag } from '../entities/ItemTag.js'; import { ItemRelation } from '../entities/ItemRelation.js'; import { EnhancedAIService } from '../services/enhanced-ai.service.js'; import { ExportManager } from '../services/export-manager.js'; const server = new Server({ name: 'shirokuma-knowledge-base', version: '0.9.0', }, { capabilities: { tools: {}, }, }); let itemRepo; let stateRepo; let exportManager; async function initializeDatabase() { if (!AppDataSource.isInitialized) { await AppDataSource.initialize(); console.error('Database initialized'); } itemRepo = new ItemRepository(); stateRepo = new SystemStateRepository(); exportManager = new ExportManager(); } const TOOLS = [ { name: 'create_item', description: 'Create a new item', inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'Item type (lowercase, numbers, underscores only)' }, title: { type: 'string', description: 'Item title' }, description: { type: 'string', description: 'Item description' }, content: { type: 'string', description: 'Detailed content' }, status: { type: 'string', description: 'Status name', default: 'Open' }, priority: { type: 'string', enum: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'MINIMAL'], default: 'MEDIUM' }, category: { type: 'string', description: 'Category' }, tags: { type: 'array', items: { type: 'string' }, description: 'Tags' }, related: { type: 'array', items: { type: 'number' }, description: 'Related item IDs' }, }, required: ['type', 'title'], }, }, { name: 'get_item', description: 'Get an item by ID', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Item ID' }, }, required: ['id'], }, }, { name: 'update_item', description: 'Update an existing item', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Item ID' }, type: { type: 'string' }, title: { type: 'string' }, description: { type: 'string' }, content: { type: 'string' }, status: { type: 'string' }, priority: { type: 'string', enum: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'MINIMAL'] }, category: { type: 'string' }, tags: { type: 'array', items: { type: 'string' } }, related: { type: 'array', items: { type: 'number' } }, }, required: ['id'], }, }, { name: 'delete_item', description: 'Delete an item', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Item ID' }, }, required: ['id'], }, }, { name: 'list_items', description: 'List items with optional filtering', inputSchema: { type: 'object', properties: { type: { type: 'string' }, status: { type: 'string' }, limit: { type: 'number', default: 20, maximum: 100 }, offset: { type: 'number', default: 0 }, }, }, }, { name: 'search_items', description: 'Search items by query', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query' }, types: { type: 'array', items: { type: 'string' } }, limit: { type: 'number', default: 20, maximum: 100 }, offset: { type: 'number', default: 0 }, }, required: ['query'], }, }, { name: 'get_current_state', description: 'Get current system state', inputSchema: { type: 'object', properties: {}, }, }, { name: 'update_current_state', description: 'Update current system state', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'State content in Markdown' }, tags: { type: 'array', items: { type: 'string' } }, metadata: { type: 'object' }, }, required: ['content'], }, }, { name: 'get_stats', description: 'Get system statistics', inputSchema: { type: 'object', properties: {}, }, }, ]; server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS, })); server.setRequestHandler(CallToolRequestSchema, async (request) => { await initializeDatabase(); const { name, arguments: args } = request.params; try { switch (name) { case 'create_item': { if (!args) { throw new McpError(ErrorCode.InvalidParams, 'Arguments required'); } let statusId = 1; if (args.status && typeof args.status === 'string') { const statusRepo = AppDataSource.getRepository(Status); let status = await statusRepo.findOne({ where: { name: args.status } }); if (!status) { status = await statusRepo.save({ name: args.status, isClosable: false, sortOrder: 0 }); } statusId = status.id; } const item = await itemRepo.create({ type: args.type ? String(args.type) : 'issue', title: args.title ? String(args.title) : 'Untitled', description: args.description ? String(args.description) : '', content: args.content ? String(args.content) : '', statusId, priority: args.priority ? String(args.priority) : 'MEDIUM', category: args.category ? String(args.category) : undefined, }); const hasContent = item.title || item.description || item.content; if (hasContent) { try { const aiService = new EnhancedAIService(AppDataSource); await aiService.enrichItem(item); } catch (error) { console.error('AI enrichment failed:', error); item.aiSummary = `ERROR: ${error instanceof Error ? error.message : String(error)}`; } } if (args.tags && Array.isArray(args.tags) && args.tags.length > 0) { const tagRepo = AppDataSource.getRepository(Tag); const itemTagRepo = AppDataSource.getRepository(ItemTag); for (const tagName of args.tags) { let tag = await tagRepo.findOne({ where: { name: tagName } }); if (!tag) { tag = await tagRepo.save({ name: tagName }); } await itemTagRepo.save({ itemId: item.id, tagId: tag.id }); } } if (args.related && Array.isArray(args.related) && args.related.length > 0) { const relationRepo = AppDataSource.getRepository(ItemRelation); for (const targetId of args.related) { await relationRepo.save({ sourceId: item.id, targetId }); await relationRepo.save({ sourceId: targetId, targetId: item.id }); } } exportManager.autoExportItem(item).catch(error => { console.error('Auto-export failed for created item:', error); }); const { embedding, ...itemWithoutEmbedding } = item; return { content: [{ type: 'text', text: JSON.stringify(itemWithoutEmbedding, null, 2) }], }; } case 'get_item': { if (!args || typeof args.id !== 'number') { throw new McpError(ErrorCode.InvalidParams, 'Valid item ID required'); } const item = await itemRepo.findById(args.id); if (!item) { throw new McpError(ErrorCode.InvalidParams, `Item ${args.id} not found`); } const itemTagRepo = AppDataSource.getRepository(ItemTag); const tagRepo = AppDataSource.getRepository(Tag); const itemTags = await itemTagRepo.find({ where: { itemId: item.id } }); const tags = []; for (const it of itemTags) { const tag = await tagRepo.findOne({ where: { id: it.tagId } }); if (tag) tags.push(tag.name); } const relationRepo = AppDataSource.getRepository(ItemRelation); const relations = await relationRepo.find({ where: { sourceId: item.id } }); const related = relations.map(r => r.targetId); const { embedding, ...itemWithoutEmbedding } = item; const result = { ...itemWithoutEmbedding, tags, related, }; return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'update_item': { if (!args || typeof args.id !== 'number') { throw new McpError(ErrorCode.InvalidParams, 'Valid item ID required'); } const existing = await itemRepo.findById(args.id); if (!existing) { throw new McpError(ErrorCode.InvalidParams, `Item ${args.id} not found`); } if (args.status && typeof args.status === 'string') { const statusRepo = AppDataSource.getRepository(Status); let status = await statusRepo.findOne({ where: { name: args.status } }); if (!status) { status = await statusRepo.save({ name: args.status, isClosable: false, sortOrder: 0 }); } args.statusId = status.id; delete args.status; } const { tags, related, ...updateData } = args; const updated = await itemRepo.update(args.id, updateData); if (tags !== undefined) { const itemTagRepo = AppDataSource.getRepository(ItemTag); const tagRepo = AppDataSource.getRepository(Tag); await itemTagRepo.delete({ itemId: args.id }); for (const tagName of tags) { let tag = await tagRepo.findOne({ where: { name: tagName } }); if (!tag) { tag = await tagRepo.save({ name: tagName }); } await itemTagRepo.save({ itemId: args.id, tagId: tag.id }); } } if (related !== undefined) { const relationRepo = AppDataSource.getRepository(ItemRelation); await relationRepo.delete({ sourceId: args.id }); for (const targetId of related) { await relationRepo.save({ sourceId: args.id, targetId }); await relationRepo.save({ sourceId: targetId, targetId: args.id }); } } if (updated) { exportManager.autoExportItem(updated).catch(error => { console.error('Auto-export failed for updated item:', error); }); } if (updated) { const { embedding, ...updatedWithoutEmbedding } = updated; return { content: [{ type: 'text', text: JSON.stringify(updatedWithoutEmbedding, null, 2) }], }; } return { content: [{ type: 'text', text: JSON.stringify(updated, null, 2) }], }; } case 'delete_item': { if (!args || typeof args.id !== 'number') { throw new McpError(ErrorCode.InvalidParams, 'Valid item ID required'); } const success = await itemRepo.delete(args.id); return { content: [{ type: 'text', text: success ? 'Item deleted' : 'Item not found' }], }; } case 'list_items': { const safeArgs = args || {}; const items = await itemRepo.findAll({ type: safeArgs.type ? String(safeArgs.type) : undefined, status: safeArgs.status ? String(safeArgs.status) : undefined, limit: typeof safeArgs.limit === 'number' ? safeArgs.limit : 20, offset: typeof safeArgs.offset === 'number' ? safeArgs.offset : 0, includeClosable: typeof safeArgs.includeClosable === 'boolean' ? safeArgs.includeClosable : undefined, onlyActive: typeof safeArgs.onlyActive === 'boolean' ? safeArgs.onlyActive : undefined, }); const sanitized = items.map(item => { const { content, embedding, ...rest } = item; return rest; }); return { content: [{ type: 'text', text: JSON.stringify(sanitized, null, 2) }], }; } case 'search_items': { if (!args || typeof args.query !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Search query required'); } const items = await itemRepo.search(args.query); let filtered = items; if (args.types && Array.isArray(args.types) && args.types.length > 0) { const types = args.types; filtered = items.filter(item => types.includes(item.type)); } const start = typeof args.offset === 'number' ? args.offset : 0; const limit = typeof args.limit === 'number' ? args.limit : 20; const paginated = filtered.slice(start, start + limit); const sanitized = paginated.map(item => { const { content, embedding, ...rest } = item; return rest; }); return { content: [{ type: 'text', text: JSON.stringify(sanitized, null, 2) }], }; } case 'get_current_state': { const state = await stateRepo.getCurrent(); if (!state) { return { content: [{ type: 'text', text: JSON.stringify({ message: 'No current state' }, null, 2) }], }; } const result = { ...state, tags: state.tags ? JSON.parse(state.tags) : [], metadata: state.metadata ? JSON.parse(state.metadata) : {}, }; return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } case 'update_current_state': { if (!args || typeof args.content !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Content required'); } const newState = await stateRepo.create({ content: args.content, tags: args.tags ? JSON.stringify(args.tags) : '[]', metadata: args.metadata ? JSON.stringify(args.metadata) : undefined, version: '0.9.0', isActive: true, }); exportManager.autoExportCurrentState(newState).catch(error => { console.error('Auto-export failed for current state:', error); }); return { content: [{ type: 'text', text: JSON.stringify(newState, null, 2) }], }; } case 'get_stats': { const itemCount = await itemRepo.count(); const statusRepo = AppDataSource.getRepository(Status); const tagRepo = AppDataSource.getRepository(Tag); const statusCount = await statusRepo.count(); const tagCount = await tagRepo.count(); const stats = { items: itemCount, statuses: statusCount, tags: tagCount, version: '0.9.0', }; return { content: [{ type: 'text', text: JSON.stringify(stats, null, 2) }], }; } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`); } }); export async function startServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP server started (TypeORM version)'); } if (import.meta.url === `file://${process.argv[1]}`) { startServer().catch((error) => { console.error('Server error:', error); process.exit(1); }); }