@sofianedjerbi/knowledge-tree-mcp
Version:
MCP server for hierarchical project knowledge management
244 lines • 9.69 kB
JavaScript
/**
* 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