Skip to main content
Glama

documcp

by tosin2013
knowledge-graph.ts33.4 kB
/** * Knowledge Graph Architecture for DocuMCP * Implements Phase 1.1: Enhanced Knowledge Graph Schema Implementation * Previously: Issue #48: Knowledge Graph Architecture * * Creates entity relationship graphs for projects, technologies, patterns, and dependencies * to enable advanced reasoning and recommendation improvements. * * Enhanced with comprehensive entity types and relationship schemas following NEW_PRD.md */ import { MemoryManager } from "./manager.js"; import { MemoryEntry } from "./storage.js"; import { validateEntity, validateRelationship, SCHEMA_METADATA, } from "./schemas.js"; export interface GraphNode { id: string; type: | "project" | "technology" | "pattern" | "user" | "outcome" | "recommendation" | "configuration" | "documentation" | "code_file" | "documentation_section" | "link_validation" | "sync_event"; label: string; properties: Record<string, any>; weight: number; lastUpdated: string; } export interface GraphEdge { id: string; source: string; target: string; type: | "uses" | "similar_to" | "depends_on" | "recommends" | "results_in" | "created_by" | "project_uses_technology" | "user_prefers_ssg" | "project_deployed_with" | "documents" | "references" | "outdated_for" | "has_link_validation" | "requires_fix" | (string & NonNullable<unknown>); // Allow any string (for timestamped types like "project_deployed_with:2024-...") weight: number; properties: Record<string, any>; confidence: number; lastUpdated: string; } export interface GraphPath { nodes: GraphNode[]; edges: GraphEdge[]; totalWeight: number; confidence: number; } export interface GraphQuery { nodeTypes?: string[]; edgeTypes?: string[]; properties?: Record<string, any>; minWeight?: number; maxDepth?: number; startNode?: string; } export interface RecommendationPath { from: GraphNode; to: GraphNode; path: GraphPath; reasoning: string[]; confidence: number; } export class KnowledgeGraph { private nodes: Map<string, GraphNode>; private edges: Map<string, GraphEdge>; private adjacencyList: Map<string, Set<string>>; private memoryManager: MemoryManager; private lastUpdate: string; constructor(memoryManager: MemoryManager) { this.nodes = new Map(); this.edges = new Map(); this.adjacencyList = new Map(); this.memoryManager = memoryManager; this.lastUpdate = new Date().toISOString(); } async initialize(): Promise<void> { await this.loadFromMemory(); await this.buildFromMemories(); } /** * Add or update a node in the knowledge graph */ addNode(node: Omit<GraphNode, "lastUpdated">): GraphNode { const fullNode: GraphNode = { ...node, lastUpdated: new Date().toISOString(), }; this.nodes.set(node.id, fullNode); if (!this.adjacencyList.has(node.id)) { this.adjacencyList.set(node.id, new Set()); } return fullNode; } /** * Add or update an edge in the knowledge graph */ addEdge(edge: Omit<GraphEdge, "id" | "lastUpdated">): GraphEdge { const edgeId = `${edge.source}-${edge.type}-${edge.target}`; const fullEdge: GraphEdge = { ...edge, id: edgeId, lastUpdated: new Date().toISOString(), }; this.edges.set(edgeId, fullEdge); // Update adjacency list if (!this.adjacencyList.has(edge.source)) { this.adjacencyList.set(edge.source, new Set()); } if (!this.adjacencyList.has(edge.target)) { this.adjacencyList.set(edge.target, new Set()); } this.adjacencyList.get(edge.source)!.add(edge.target); return fullEdge; } /** * Build knowledge graph from memory entries */ async buildFromMemories(): Promise<void> { const memories = await this.memoryManager.search("", { sortBy: "timestamp", }); for (const memory of memories) { await this.processMemoryEntry(memory); } await this.computeRelationships(); await this.updateWeights(); } /** * Process a single memory entry to extract graph entities */ private async processMemoryEntry(memory: MemoryEntry): Promise<void> { // Create project node if (memory.metadata.projectId) { const projectNode = this.addNode({ id: `project:${memory.metadata.projectId}`, type: "project", label: memory.metadata.projectId, properties: { repository: memory.metadata.repository, lastActivity: memory.timestamp, }, weight: 1.0, }); // Create technology nodes if (memory.type === "analysis" && memory.data.language) { const langNode = this.addNode({ id: `tech:${memory.data.language.primary}`, type: "technology", label: memory.data.language.primary, properties: { category: "language", popularity: this.getTechnologyPopularity( memory.data.language.primary, ), }, weight: 1.0, }); this.addEdge({ source: projectNode.id, target: langNode.id, type: "uses", weight: 1.0, confidence: 0.9, properties: { source: "analysis" }, }); } // Create framework nodes if (memory.data.framework?.name) { const frameworkNode = this.addNode({ id: `tech:${memory.data.framework.name}`, type: "technology", label: memory.data.framework.name, properties: { category: "framework", version: memory.data.framework.version, }, weight: 1.0, }); this.addEdge({ source: projectNode.id, target: frameworkNode.id, type: "uses", weight: 1.0, confidence: 0.8, properties: { source: "analysis" }, }); } // Create SSG recommendation nodes if (memory.type === "recommendation" && memory.data.recommended) { const ssgNode = this.addNode({ id: `tech:${memory.data.recommended}`, type: "technology", label: memory.data.recommended, properties: { category: "ssg", score: memory.data.score, }, weight: 1.0, }); this.addEdge({ source: projectNode.id, target: ssgNode.id, type: "recommends", weight: memory.data.score || 1.0, confidence: memory.data.confidence || 0.5, properties: { source: "recommendation", reasoning: memory.data.reasoning, }, }); } // Create outcome nodes if (memory.type === "deployment") { const outcomeNode = this.addNode({ id: `outcome:${memory.data.status}:${memory.metadata.ssg}`, type: "outcome", label: `${memory.data.status} with ${memory.metadata.ssg}`, properties: { status: memory.data.status, ssg: memory.metadata.ssg, duration: memory.data.duration, }, weight: memory.data.status === "success" ? 1.0 : 0.5, }); this.addEdge({ source: projectNode.id, target: outcomeNode.id, type: "results_in", weight: 1.0, confidence: 1.0, properties: { timestamp: memory.timestamp, details: memory.data.details, }, }); } } } /** * Compute additional relationships based on patterns */ private async computeRelationships(): Promise<void> { // Find similar projects await this.computeProjectSimilarity(); // Find technology dependencies await this.computeTechnologyDependencies(); // Find pattern relationships await this.computePatternRelationships(); } /** * Compute project similarity relationships */ private async computeProjectSimilarity(): Promise<void> { const projectNodes = Array.from(this.nodes.values()).filter( (node) => node.type === "project", ); for (let i = 0; i < projectNodes.length; i++) { for (let j = i + 1; j < projectNodes.length; j++) { const similarity = this.calculateProjectSimilarity( projectNodes[i], projectNodes[j], ); if (similarity > 0.7) { this.addEdge({ source: projectNodes[i].id, target: projectNodes[j].id, type: "similar_to", weight: similarity, confidence: similarity, properties: { computed: true, similarityScore: similarity, }, }); } } } } /** * Calculate similarity between two projects */ private calculateProjectSimilarity( project1: GraphNode, project2: GraphNode, ): number { const tech1 = this.getConnectedTechnologies(project1.id); const tech2 = this.getConnectedTechnologies(project2.id); if (tech1.size === 0 || tech2.size === 0) return 0; const intersection = new Set([...tech1].filter((x) => tech2.has(x))); const union = new Set([...tech1, ...tech2]); return intersection.size / union.size; // Jaccard similarity } /** * Get technologies connected to a project */ private getConnectedTechnologies(projectId: string): Set<string> { const technologies = new Set<string>(); const adjacents = this.adjacencyList.get(projectId) || new Set(); for (const nodeId of adjacents) { const node = this.nodes.get(nodeId); if (node && node.type === "technology") { technologies.add(nodeId); } } return technologies; } /** * Compute technology dependency relationships */ private async computeTechnologyDependencies(): Promise<void> { // Define known technology dependencies const dependencies = new Map([ ["tech:react", ["tech:javascript", "tech:nodejs"]], ["tech:vue", ["tech:javascript", "tech:nodejs"]], ["tech:angular", ["tech:typescript", "tech:nodejs"]], ["tech:gatsby", ["tech:react", "tech:graphql"]], ["tech:next.js", ["tech:react", "tech:nodejs"]], ["tech:nuxt.js", ["tech:vue", "tech:nodejs"]], ["tech:docusaurus", ["tech:react", "tech:markdown"]], ["tech:jekyll", ["tech:ruby", "tech:markdown"]], ["tech:hugo", ["tech:go", "tech:markdown"]], ["tech:mkdocs", ["tech:python", "tech:markdown"]], ]); for (const [tech, deps] of dependencies) { for (const dep of deps) { const techNode = this.nodes.get(tech); const depNode = this.nodes.get(dep); if (techNode && depNode) { this.addEdge({ source: tech, target: dep, type: "depends_on", weight: 0.8, confidence: 0.9, properties: { computed: true, dependency_type: "runtime", }, }); } } } } /** * Compute pattern relationships from successful combinations */ private async computePatternRelationships(): Promise<void> { const successfulOutcomes = Array.from(this.nodes.values()).filter( (node) => node.type === "outcome" && node.properties.status === "success", ); for (const outcome of successfulOutcomes) { // Find the path that led to this successful outcome const incomingEdges = Array.from(this.edges.values()).filter( (edge) => edge.target === outcome.id, ); for (const edge of incomingEdges) { const sourceNode = this.nodes.get(edge.source); if (sourceNode && sourceNode.type === "project") { // Strengthen relationships for successful patterns this.strengthenSuccessPattern(sourceNode.id, outcome.properties.ssg); } } } } /** * Strengthen relationships for successful patterns */ private strengthenSuccessPattern(projectId: string, ssg: string): void { const ssgNodeId = `tech:${ssg}`; const edgeId = `${projectId}-recommends-${ssgNodeId}`; const edge = this.edges.get(edgeId); if (edge) { edge.weight = Math.min(edge.weight * 1.2, 2.0); edge.confidence = Math.min(edge.confidence * 1.1, 1.0); } } /** * Update node and edge weights based on usage patterns */ private async updateWeights(): Promise<void> { // Update node weights based on connections for (const node of this.nodes.values()) { const connections = this.adjacencyList.get(node.id)?.size || 0; node.weight = Math.log(connections + 1) / Math.log(10); // Logarithmic scaling } // Update edge weights based on frequency and success for (const edge of this.edges.values()) { if (edge.type === "recommends") { // Find successful outcomes for this recommendation const targetNode = this.nodes.get(edge.target); if (targetNode && targetNode.type === "technology") { const successRate = this.calculateSuccessRate(targetNode.id); edge.weight *= 1 + successRate; } } } } /** * Calculate success rate for a technology */ private calculateSuccessRate(techId: string): number { const tech = techId.replace("tech:", ""); const outcomes = Array.from(this.nodes.values()).filter( (node) => node.type === "outcome" && node.properties.ssg === tech, ); if (outcomes.length === 0) return 0; const successes = outcomes.filter( (node) => node.properties.status === "success", ).length; return successes / outcomes.length; } /** * Find the shortest path between two nodes */ findPath( sourceId: string, targetId: string, maxDepth: number = 5, ): GraphPath | null { const visited = new Set<string>(); const queue: { nodeId: string; path: GraphPath }[] = [ { nodeId: sourceId, path: { nodes: [this.nodes.get(sourceId)!], edges: [], totalWeight: 0, confidence: 1.0, }, }, ]; while (queue.length > 0) { const current = queue.shift()!; if (current.nodeId === targetId) { return current.path; } if (current.path.nodes.length >= maxDepth) { continue; } visited.add(current.nodeId); const neighbors = this.adjacencyList.get(current.nodeId) || new Set(); for (const neighborId of neighbors) { if (visited.has(neighborId)) continue; const edge = this.findEdge(current.nodeId, neighborId); const neighborNode = this.nodes.get(neighborId); if (edge && neighborNode) { const newPath: GraphPath = { nodes: [...current.path.nodes, neighborNode], edges: [...current.path.edges, edge], totalWeight: current.path.totalWeight + edge.weight, confidence: current.path.confidence * edge.confidence, }; queue.push({ nodeId: neighborId, path: newPath }); } } } return null; } /** * Find edge between two nodes */ private findEdge(sourceId: string, targetId: string): GraphEdge | null { for (const edge of this.edges.values()) { if (edge.source === sourceId && edge.target === targetId) { return edge; } } return null; } /** * Query the knowledge graph */ query(query: GraphQuery): { nodes: GraphNode[]; edges: GraphEdge[]; paths?: GraphPath[]; } { let nodes = Array.from(this.nodes.values()); let edges = Array.from(this.edges.values()); // Filter by node types if (query.nodeTypes) { nodes = nodes.filter((node) => query.nodeTypes!.includes(node.type)); } // Filter by edge types if (query.edgeTypes) { edges = edges.filter((edge) => query.edgeTypes!.includes(edge.type)); } // Filter by properties if (query.properties) { nodes = nodes.filter((node) => Object.entries(query.properties!).every( ([key, value]) => node.properties[key] === value, ), ); } // Filter by minimum weight if (query.minWeight) { nodes = nodes.filter((node) => node.weight >= query.minWeight!); edges = edges.filter((edge) => edge.weight >= query.minWeight!); } const result = { nodes, edges }; // Find paths from start node if specified if (query.startNode && query.maxDepth) { const paths: GraphPath[] = []; const visited = new Set<string>(); const emptyPath: GraphPath = { nodes: [], edges: [], totalWeight: 0, confidence: 1.0, }; this.explorePaths( query.startNode, emptyPath, paths, visited, query.maxDepth, ); (result as any).paths = paths; } return result; } /** * Explore paths from a starting node */ private explorePaths( nodeId: string, currentPath: GraphPath, allPaths: GraphPath[], visited: Set<string>, maxDepth: number, ): void { if (currentPath.nodes.length >= maxDepth) return; visited.add(nodeId); const neighbors = this.adjacencyList.get(nodeId) || new Set(); for (const neighborId of neighbors) { if (visited.has(neighborId)) continue; const edge = this.findEdge(nodeId, neighborId); const neighborNode = this.nodes.get(neighborId); if (edge && neighborNode) { const newPath: GraphPath = { nodes: [...(currentPath.nodes || []), neighborNode], edges: [...(currentPath.edges || []), edge], totalWeight: (currentPath.totalWeight || 0) + edge.weight, confidence: (currentPath.confidence || 1.0) * edge.confidence, }; allPaths.push(newPath); this.explorePaths( neighborId, newPath, allPaths, new Set(visited), maxDepth, ); } } } /** * Get enhanced recommendations using knowledge graph */ async getGraphBasedRecommendation( projectFeatures: any, candidateSSGs: string[], ): Promise<RecommendationPath[]> { const recommendations: RecommendationPath[] = []; // Create a temporary project node const tempProjectId = `temp:${Date.now()}`; const projectNode = this.addNode({ id: tempProjectId, type: "project", label: "Query Project", properties: projectFeatures, weight: 1.0, }); for (const ssg of candidateSSGs) { const ssgNodeId = `tech:${ssg}`; const ssgNode = this.nodes.get(ssgNodeId); if (ssgNode) { // Find paths from similar projects to this SSG const similarProjects = this.findSimilarProjects(projectFeatures); for (const similarProject of similarProjects) { const path = this.findPath(similarProject.id, ssgNodeId); if (path) { const reasoning = this.generateReasoning(path); const confidence = this.calculatePathConfidence( path, projectFeatures, ); recommendations.push({ from: projectNode, to: ssgNode, path, reasoning, confidence, }); } } } } // Clean up temporary node this.nodes.delete(tempProjectId); return recommendations.sort((a, b) => b.confidence - a.confidence); } /** * Find projects similar to given features */ private findSimilarProjects(features: any): GraphNode[] { const projectNodes = Array.from(this.nodes.values()).filter( (node) => node.type === "project", ); return projectNodes .map((project) => ({ project, similarity: this.calculateFeatureSimilarity( features, project.properties, ), })) .filter(({ similarity }) => similarity > 0.6) .sort((a, b) => b.similarity - a.similarity) .slice(0, 5) .map(({ project }) => project); } /** * Calculate similarity between features and project properties */ private calculateFeatureSimilarity(features: any, properties: any): number { let score = 0; let factors = 0; if (features.language === properties.language) { score += 0.4; } factors++; if (features.framework === properties.framework) { score += 0.3; } factors++; if (features.size === properties.size) { score += 0.2; } factors++; if (features.complexity === properties.complexity) { score += 0.1; } factors++; return factors > 0 ? score / factors : 0; } /** * Generate human-readable reasoning for a recommendation path */ private generateReasoning(path: GraphPath): string[] { const reasoning: string[] = []; for (let i = 0; i < path.edges.length; i++) { const edge = path.edges[i]; const sourceNode = path.nodes[i]; const targetNode = path.nodes[i + 1]; switch (edge.type) { case "similar_to": reasoning.push( `Similar to ${sourceNode.label} (${(edge.confidence * 100).toFixed( 0, )}% similarity)`, ); break; case "recommends": reasoning.push( `Successfully used ${ targetNode.label } (score: ${edge.weight.toFixed(1)})`, ); break; case "results_in": reasoning.push( `Resulted in ${targetNode.properties.status} deployment`, ); break; case "uses": reasoning.push(`Uses ${targetNode.label}`); break; } } return reasoning; } /** * Calculate confidence for a recommendation path */ private calculatePathConfidence(path: GraphPath, _features: any): number { let confidence = path.confidence; // Boost confidence for shorter paths confidence *= 1 / Math.max(path.edges.length, 1); // Boost confidence for recent data const avgAge = path.nodes.reduce((sum, node) => { const age = Date.now() - new Date(node.lastUpdated).getTime(); return sum + age; }, 0) / path.nodes.length; const daysSinceUpdate = avgAge / (1000 * 60 * 60 * 24); confidence *= Math.exp(-daysSinceUpdate / 30); // Exponential decay over 30 days return Math.min(confidence, 1.0); } /** * Get technology popularity score */ private getTechnologyPopularity(tech: string): number { // Simple popularity scoring - could be enhanced with real data const popularityMap = new Map([ ["javascript", 0.9], ["typescript", 0.8], ["python", 0.8], ["react", 0.9], ["vue", 0.7], ["angular", 0.6], ["go", 0.7], ["ruby", 0.5], ["rust", 0.6], ]); return popularityMap.get(tech.toLowerCase()) || 0.3; } /** * Save knowledge graph to persistent memory */ async saveToMemory(): Promise<void> { const graphData = { nodes: Array.from(this.nodes.entries()), edges: Array.from(this.edges.entries()), lastUpdate: this.lastUpdate, statistics: this.getStatistics(), }; await this.memoryManager.remember( "interaction", { graph: graphData, type: "knowledge_graph", }, { tags: ["knowledge_graph", "structure"], }, ); } /** * Load knowledge graph from persistent memory */ async loadFromMemory(): Promise<void> { try { const graphMemories = await this.memoryManager.search("knowledge_graph"); if (graphMemories.length > 0) { const latestGraph = graphMemories.sort( (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), )[0]; if (latestGraph.data.graph) { const { nodes, edges } = latestGraph.data.graph; // Restore nodes for (const [id, node] of nodes) { this.nodes.set(id, node); } // Restore edges and adjacency list for (const [id, edge] of edges) { this.edges.set(id, edge); if (!this.adjacencyList.has(edge.source)) { this.adjacencyList.set(edge.source, new Set()); } this.adjacencyList.get(edge.source)!.add(edge.target); } this.lastUpdate = latestGraph.data.graph.lastUpdate; } } } catch (error) { console.error("Failed to load knowledge graph from memory:", error); } } /** * Get all nodes in the knowledge graph */ async getAllNodes(): Promise<GraphNode[]> { return Array.from(this.nodes.values()); } /** * Get all edges in the knowledge graph */ async getAllEdges(): Promise<GraphEdge[]> { return Array.from(this.edges.values()); } /** * Remove a node from the knowledge graph */ async removeNode(nodeId: string): Promise<boolean> { const node = this.nodes.get(nodeId); if (!node) { return false; } // Remove the node this.nodes.delete(nodeId); // Remove all edges connected to this node const edgesToRemove: string[] = []; for (const [edgeId, edge] of this.edges) { if (edge.source === nodeId || edge.target === nodeId) { edgesToRemove.push(edgeId); } } for (const edgeId of edgesToRemove) { this.edges.delete(edgeId); } // Update adjacency list this.adjacencyList.delete(nodeId); for (const [, targets] of this.adjacencyList) { targets.delete(nodeId); } return true; } /** * Get connections for a specific node */ async getConnections(nodeId: string): Promise<string[]> { const connections = this.adjacencyList.get(nodeId); return connections ? Array.from(connections) : []; } /** * Get knowledge graph statistics */ async getStatistics(): Promise<{ nodeCount: number; edgeCount: number; nodesByType: Record<string, number>; edgesByType: Record<string, number>; averageConnectivity: number; mostConnectedNodes: Array<{ id: string; connections: number }>; }> { const nodesByType: Record<string, number> = {}; const edgesByType: Record<string, number> = {}; for (const node of this.nodes.values()) { nodesByType[node.type] = (nodesByType[node.type] || 0) + 1; } for (const edge of this.edges.values()) { edgesByType[edge.type] = (edgesByType[edge.type] || 0) + 1; } const connectivityCounts = Array.from(this.adjacencyList.entries()) .map(([id, connections]) => ({ id, connections: connections.size })) .sort((a, b) => b.connections - a.connections); const averageConnectivity = connectivityCounts.length > 0 ? connectivityCounts.reduce( (sum, { connections }) => sum + connections, 0, ) / connectivityCounts.length : 0; return { nodeCount: this.nodes.size, edgeCount: this.edges.size, nodesByType, edgesByType, averageConnectivity, mostConnectedNodes: connectivityCounts.slice(0, 10), }; } // ============================================================================ // Phase 1.1: Enhanced Node Query Methods // ============================================================================ /** * Find a single node matching criteria */ async findNode(criteria: { type?: string; properties?: Record<string, any>; }): Promise<GraphNode | null> { for (const node of this.nodes.values()) { if (criteria.type && node.type !== criteria.type) continue; if (criteria.properties) { let matches = true; for (const [key, value] of Object.entries(criteria.properties)) { if (node.properties[key] !== value) { matches = false; break; } } if (!matches) continue; } return node; } return null; } /** * Find all nodes matching criteria */ async findNodes(criteria: { type?: string; properties?: Record<string, any>; }): Promise<GraphNode[]> { const results: GraphNode[] = []; for (const node of this.nodes.values()) { if (criteria.type && node.type !== criteria.type) continue; if (criteria.properties) { let matches = true; for (const [key, value] of Object.entries(criteria.properties)) { if (node.properties[key] !== value) { matches = false; break; } } if (!matches) continue; } results.push(node); } return results; } /** * Find edges matching criteria */ async findEdges(criteria: { source?: string; target?: string; type?: string; properties?: Record<string, any>; }): Promise<GraphEdge[]> { const results: GraphEdge[] = []; for (const edge of this.edges.values()) { if (criteria.source && edge.source !== criteria.source) continue; if (criteria.target && edge.target !== criteria.target) continue; if (criteria.type && edge.type !== criteria.type) continue; // Match properties if provided if (criteria.properties) { let propertiesMatch = true; for (const [key, value] of Object.entries(criteria.properties)) { if (edge.properties[key] !== value) { propertiesMatch = false; break; } } if (!propertiesMatch) continue; } results.push(edge); } return results; } /** * Find all paths between two nodes up to a maximum depth */ async findPaths(criteria: { startNode: string; endNode?: string; edgeTypes?: string[]; maxDepth: number; }): Promise<GraphPath[]> { const paths: GraphPath[] = []; const visited = new Set<string>(); const emptyPath: GraphPath = { nodes: [this.nodes.get(criteria.startNode)!], edges: [], totalWeight: 0, confidence: 1.0, }; this.findPathsRecursive( criteria.startNode, emptyPath, paths, visited, criteria.maxDepth, criteria.endNode, criteria.edgeTypes, ); return paths; } /** * Recursive helper for finding paths */ private findPathsRecursive( currentNodeId: string, currentPath: GraphPath, allPaths: GraphPath[], visited: Set<string>, maxDepth: number, endNode?: string, edgeTypes?: string[], ): void { if (currentPath.nodes.length >= maxDepth) return; visited.add(currentNodeId); const neighbors = this.adjacencyList.get(currentNodeId) || new Set(); for (const neighborId of neighbors) { if (visited.has(neighborId)) continue; const edge = this.findEdge(currentNodeId, neighborId); if (!edge) continue; // Filter by edge type if specified if (edgeTypes && !edgeTypes.includes(edge.type)) continue; const neighborNode = this.nodes.get(neighborId); if (!neighborNode) continue; const newPath: GraphPath = { nodes: [...currentPath.nodes, neighborNode], edges: [...currentPath.edges, edge], totalWeight: currentPath.totalWeight + edge.weight, confidence: currentPath.confidence * edge.confidence, }; // If we've reached the end node, add this path if (endNode && neighborId === endNode) { allPaths.push(newPath); continue; } // If no end node specified, add all paths if (!endNode) { allPaths.push(newPath); } // Continue exploring this.findPathsRecursive( neighborId, newPath, allPaths, new Set(visited), maxDepth, endNode, edgeTypes, ); } } /** * Get node history (all changes to a node over time) */ async getNodeHistory(nodeId: string): Promise<MemoryEntry[]> { const node = this.nodes.get(nodeId); if (!node) return []; // Search memory for all entries related to this node const projectId = node.properties.projectId || node.properties.path; if (!projectId) return []; return await this.memoryManager.search(projectId); } /** * Get schema version */ getSchemaVersion(): string { return SCHEMA_METADATA.version; } /** * Validate node against schema */ validateNode(node: GraphNode): boolean { try { const entityData = { ...node.properties, type: node.type, }; validateEntity(entityData); return true; } catch (error) { console.error(`Node validation failed for ${node.id}:`, error); return false; } } /** * Validate edge against schema */ validateEdge(edge: GraphEdge): boolean { try { const relationshipData = { type: edge.type, weight: edge.weight, confidence: edge.confidence, createdAt: edge.lastUpdated, lastUpdated: edge.lastUpdated, metadata: edge.properties, ...edge.properties, }; validateRelationship(relationshipData); return true; } catch (error) { console.error(`Edge validation failed for ${edge.id}:`, error); return false; } } } export default KnowledgeGraph;

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/tosin2013/documcp'

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