@sofianedjerbi/knowledge-tree-mcp
Version:
MCP server for hierarchical project knowledge management
253 lines (223 loc) • 6.56 kB
text/typescript
/**
* WebSocket message handlers for the Knowledge Tree web interface
*/
import { join } from 'path';
import type {
WebSocketMessage,
GetAllMessage,
SearchMessage,
StatsMessage,
RecentMessage,
WebContext
} from './types.js';
import { readFile } from '../utils/index.js';
import type { KnowledgeEntry } from '../types/index.js';
/**
* Handle incoming WebSocket messages
*/
export async function handleWebSocketMessage(
message: string,
ws: any,
context: WebContext
): Promise<void> {
try {
const data: WebSocketMessage = JSON.parse(message);
// Web interface activity logging disabled - we don't track UI interactions
switch (data.type) {
case 'getAll':
await handleGetAll(data as GetAllMessage, ws, context);
break;
case 'search':
await handleSearch(data as SearchMessage, ws, context);
break;
case 'stats':
await handleStats(data as StatsMessage, ws, context);
break;
case 'recent':
await handleRecent(data as RecentMessage, ws, context);
break;
default:
console.error('Unknown WebSocket message type:', data.type);
}
} catch (error) {
console.error('WebSocket message error:', error);
ws.send(JSON.stringify({
type: 'error',
message: 'Failed to process message',
error: error instanceof Error ? error.message : 'Unknown error'
}));
}
}
/**
* Handle getAll request - send all knowledge entries
*/
async function handleGetAll(
data: GetAllMessage,
ws: any,
context: WebContext
): Promise<void> {
const allEntries = await context.scanKnowledgeTree();
const entries = [];
for (const path of allEntries) {
const fullPath = join(context.knowledgeRoot, path);
try {
const content = await readFile(fullPath);
const entry: KnowledgeEntry = JSON.parse(content);
entries.push({ path, data: entry });
} catch (error) {
// Skip invalid entries
}
}
ws.send(JSON.stringify({
type: 'allEntries',
entries
}));
}
/**
* Handle search request
*/
async function handleSearch(
data: SearchMessage,
ws: any,
context: WebContext
): Promise<void> {
let query = data.query;
let searchIn = data.searchIn || ["all"];
// Handle special tag: prefix
if (query && query.startsWith('tag:')) {
const tagName = query.substring(4).trim();
query = tagName;
searchIn = ["tags"];
}
const searchArgs = {
query: query,
priority: data.priority || [],
category: data.category,
searchIn: searchIn,
regex: data.regex || false,
caseSensitive: data.caseSensitive || false,
limit: data.limit || 50,
sortBy: data.sortBy || "relevance"
};
const result = await context.searchKnowledge(searchArgs);
const markdownText = result.content[0].text;
// Parse markdown results into structured format
const results = [];
// Extract entries from markdown
// Format from search output:
// ## 📄 Title
// **Path**: `path.json`
// **Priority**: CRITICAL
// **Score**: 23
const sections = markdownText.split('\n---\n');
for (const section of sections) {
if (!section.trim() || section.includes('Total matches:')) continue;
// Extract title from ## 📄 Title line
const titleMatch = section.match(/## 📄 (.+)/);
if (!titleMatch) continue;
const title = titleMatch[1].trim();
// Extract path from **Path**: `path.json` line
const pathMatch = section.match(/\*\*Path\*\*:\s*`([^`]+)`/);
if (!pathMatch) continue;
const path = pathMatch[1];
// Extract priority from **Priority**: CRITICAL line
const priorityMatch = section.match(/\*\*Priority\*\*:\s*(\w+)/);
const priority = priorityMatch ? priorityMatch[1] : 'COMMON';
// Try to load the full entry
try {
const fullPath = join(context.knowledgeRoot, path);
const entryContent = await readFile(fullPath);
const entry = JSON.parse(entryContent);
results.push({
path: path,
entry: entry,
priority: entry.priority || priority,
title: entry.title || title,
problem: entry.problem || ''
});
} catch (error) {
// If we can't load the entry, use what we have from markdown
// Extract problem from the markdown content
const problemMatch = section.match(/# Problem\s*\n\n([^\n]+)/);
const problem = problemMatch ? problemMatch[1] : '';
results.push({
path: path,
entry: {
priority: priority,
title: title,
problem: problem
},
priority: priority,
title: title,
problem: problem
});
}
}
ws.send(JSON.stringify({
type: 'searchResults',
results: results,
total: results.length
}));
}
/**
* Handle stats request
*/
async function handleStats(
data: StatsMessage,
ws: any,
context: WebContext
): Promise<void> {
const statsArgs = {
include: data.include || ["summary", "priorities", "orphaned", "popular"]
};
// Call stats tool directly with server context instead of going through web context
const { statsKnowledgeHandler } = await import('../tools/stats.js');
const result = await statsKnowledgeHandler(statsArgs, context.serverContext);
const statsData = JSON.parse(result.content[0].text);
ws.send(JSON.stringify({
type: 'statsResults',
...statsData
}));
}
/**
* Handle recent changes request
*/
async function handleRecent(
data: RecentMessage,
ws: any,
context: WebContext
): Promise<void> {
const recentArgs = {
days: data.days || 7,
limit: data.limit || 20,
type: data.changeType || "all"
};
const result = await context.getRecentKnowledge(recentArgs);
const recentData = JSON.parse(result.content[0].text);
ws.send(JSON.stringify({
type: 'recentResults',
days: data.days || 7, // Include the days parameter from the request
...recentData
}));
}
/**
* Broadcast an update to all connected WebSocket clients
*/
export async function broadcastUpdate(
type: 'entryAdded' | 'entryUpdated' | 'entryDeleted',
data: any,
wsClients: Set<any>
): Promise<void> {
if (wsClients.size === 0) return;
const message = JSON.stringify({ type, ...data });
for (const client of wsClients) {
if (client.readyState === 1) { // WebSocket.OPEN
try {
await client.send(message);
} catch (error) {
// Remove dead clients
wsClients.delete(client);
}
}
}
}