UNPKG

@sofianedjerbi/knowledge-tree-mcp

Version:

MCP server for hierarchical project knowledge management

244 lines 9.69 kB
/** * Update knowledge tool implementation * Modifies existing knowledge entries with validation */ import { join } from 'path'; import { Constants, isBidirectionalRelationship } from '../constants/index.js'; import { ensureJsonExtension, fileExists, readKnowledgeEntry, writeKnowledgeEntry, moveEntryWithReferences, validateEntryMove, generatePathFromTitle } from '../utils/index.js'; /** * Handler for the update_knowledge tool */ export const updateKnowledgeHandler = async (args, context) => { const { path, new_path, updates, regenerate_path = false } = args; // Ensure path ends with .json const jsonPath = ensureJsonExtension(path); const fullPath = join(context.knowledgeRoot, jsonPath); // Read existing entry let entry; try { if (!await fileExists(fullPath)) { return { content: [ { type: "text", text: `❌ Entry not found: ${jsonPath}`, }, ], }; } entry = await readKnowledgeEntry(fullPath); } catch (error) { return { content: [ { type: "text", text: `❌ Failed to read entry: ${jsonPath}`, }, ], }; } // Validate updates const validationErrors = []; if (updates.title !== undefined && (!updates.title || typeof updates.title !== 'string')) { validationErrors.push("Title must be a non-empty string"); } if (updates.priority && !Constants.isValidPriority(updates.priority)) { validationErrors.push("Invalid priority value"); } if (updates.problem !== undefined && (!updates.problem || typeof updates.problem !== 'string')) { validationErrors.push("Problem must be a non-empty string"); } if (updates.solution !== undefined && (!updates.solution || typeof updates.solution !== 'string')) { validationErrors.push("Solution must be a non-empty string"); } if (updates.tags !== undefined && !Array.isArray(updates.tags)) { validationErrors.push("Tags must be an array of strings"); } // Priority is no longer part of the filename - removed validation if (validationErrors.length > 0) { return { content: [ { type: "text", text: `❌ Validation failed:\n${validationErrors.map(e => `• ${e}`).join('\n')}`, }, ], }; } // Apply updates const oldEntry = { ...entry }; if (updates.title !== undefined) entry.title = updates.title; if (updates.slug !== undefined) entry.slug = updates.slug; if (updates.priority !== undefined) entry.priority = updates.priority; if (updates.category !== undefined) entry.category = updates.category; if (updates.tags !== undefined) entry.tags = updates.tags; if (updates.problem !== undefined) entry.problem = updates.problem; if (updates.context !== undefined) entry.context = updates.context; if (updates.solution !== undefined) entry.solution = updates.solution; if (updates.examples !== undefined) entry.examples = updates.examples; if (updates.code !== undefined) entry.code = updates.code; if (updates.author !== undefined) entry.author = updates.author; if (updates.version !== undefined) entry.version = updates.version; // Update the updated_at timestamp entry.updated_at = new Date().toISOString(); // Check if path change is needed (explicit or regenerated) let finalPath = jsonPath; let pathChanged = false; let targetPath = null; // Handle explicit new_path parameter if (new_path) { targetPath = ensureJsonExtension(new_path); } // Handle automatic path regeneration else if (regenerate_path && entry.title) { targetPath = generatePathFromTitle(entry.title, { category: entry.category, tags: entry.tags, priority: entry.priority }); } if (targetPath && targetPath !== jsonPath) { // Validate the move const moveValidation = await validateEntryMove(jsonPath, targetPath, context); if (!moveValidation.valid) { return { content: [ { type: "text", text: `❌ Cannot move entry to ${targetPath}: ${moveValidation.warnings.join(', ')}`, }, ], }; } // Perform the move const moveResult = await moveEntryWithReferences(jsonPath, targetPath, context); if (!moveResult.success) { return { content: [ { type: "text", text: `❌ Failed to move entry: ${moveResult.error}`, }, ], }; } finalPath = targetPath; pathChanged = true; // Update the entry with the new path and save it const newFullPath = join(context.knowledgeRoot, finalPath); await writeKnowledgeEntry(newFullPath, entry); // Report what was updated including path change const updatedFields = Object.keys(updates).filter(key => updates[key] !== undefined); const pathInfo = moveValidation.warnings.length > 0 ? `\n⚠️ ${moveValidation.warnings.join('\n⚠️ ')}` : ''; const moveType = new_path ? 'moved to explicit path' : 'moved with regenerated path'; return { content: [ { type: "text", text: `✅ Successfully updated and ${moveType}\n📁 Old path: ${jsonPath}\n📁 New path: ${finalPath}\n📝 Updated fields: ${updatedFields.join(', ')}${pathInfo}`, }, ], }; } // Handle relationship updates if (updates.related_to !== undefined) { // Validate new relationships for (const link of updates.related_to) { const linkPath = ensureJsonExtension(link.path); const linkFullPath = join(context.knowledgeRoot, linkPath); if (!await fileExists(linkFullPath)) { validationErrors.push(`Linked entry does not exist: ${linkPath}`); } } if (validationErrors.length > 0) { return { content: [ { type: "text", text: `❌ Validation failed:\n${validationErrors.map(e => `• ${e}`).join('\n')}`, }, ], }; } // Remove old bidirectional links if (oldEntry.related_to) { for (const oldLink of oldEntry.related_to) { if (isBidirectionalRelationship(oldLink.relationship)) { // Remove reverse link try { const targetPath = join(context.knowledgeRoot, oldLink.path); const targetEntry = await readKnowledgeEntry(targetPath); if (targetEntry.related_to) { targetEntry.related_to = targetEntry.related_to.filter(link => link.path !== jsonPath); await writeKnowledgeEntry(targetPath, targetEntry); } } catch (error) { // Continue if we can't update } } } } entry.related_to = updates.related_to; // Create new bidirectional links if (entry.related_to) { for (const link of entry.related_to) { if (isBidirectionalRelationship(link.relationship)) { try { const targetPath = join(context.knowledgeRoot, link.path); const targetEntry = await readKnowledgeEntry(targetPath); if (!targetEntry.related_to) { targetEntry.related_to = []; } const reverseExists = targetEntry.related_to.some(reverseLink => reverseLink.path === jsonPath); if (!reverseExists) { targetEntry.related_to.push({ path: jsonPath, relationship: link.relationship, description: link.description }); await writeKnowledgeEntry(targetPath, targetEntry); } } catch (error) { // Continue if we can't create reverse link } } } } } // Save updated entry (only if path didn't change) if (!pathChanged) { await writeKnowledgeEntry(fullPath, entry); // Broadcast update await context.broadcastUpdate('entryUpdated', { path: finalPath, data: entry }); } // Report what was updated const updatedFields = Object.keys(updates).filter(key => updates[key] !== undefined); return { content: [ { type: "text", text: `✅ Successfully updated ${finalPath}\n📝 Updated fields: ${updatedFields.join(', ')}`, }, ], }; }; //# sourceMappingURL=update.js.map