Skip to main content
Glama

Obsidian MCP Server

by bazylhorsey
KnowledgeGraph.ts•8.76 kB
/** * Knowledge graph service for analyzing note relationships */ import type { Note, KnowledgeGraph, GraphNode, GraphEdge } from '../types/index.js'; export interface GraphAnalysis { totalNodes: number; totalEdges: number; mostConnectedNodes: Array<{ id: string; connections: number }>; orphanNotes: string[]; clusters?: Array<string[]>; } export class KnowledgeGraphService { private notes: Map<string, Note> = new Map(); /** * Update the knowledge graph with new notes */ updateNotes(notes: Note[]): void { this.notes.clear(); for (const note of notes) { this.notes.set(note.path, note); } } /** * Build a complete knowledge graph */ buildGraph(): KnowledgeGraph { const nodes: GraphNode[] = []; const edges: GraphEdge[] = []; const tagNodes = new Map<string, GraphNode>(); const folderNodes = new Map<string, GraphNode>(); // Create note nodes for (const note of this.notes.values()) { nodes.push({ id: note.path, title: note.title, type: 'note', metadata: { tags: note.tags, wordCount: note.content.length, linkCount: note.links?.length || 0 } }); // Create edges for note links if (note.links) { for (const link of note.links) { edges.push({ source: note.path, target: this.resolveNotePath(link.target), type: link.type, weight: 1 }); } } // Create tag nodes and edges if (note.tags) { for (const tag of note.tags) { if (!tagNodes.has(tag)) { const tagNode: GraphNode = { id: `tag:${tag}`, title: tag, type: 'tag' }; tagNodes.set(tag, tagNode); nodes.push(tagNode); } edges.push({ source: note.path, target: `tag:${tag}`, type: 'tagged-with', weight: 1 }); } } // Create folder nodes and edges const folder = this.getFolder(note.path); if (folder && folder !== '.') { if (!folderNodes.has(folder)) { const folderNode: GraphNode = { id: `folder:${folder}`, title: folder, type: 'folder' }; folderNodes.set(folder, folderNode); nodes.push(folderNode); } edges.push({ source: note.path, target: `folder:${folder}`, type: 'in-folder', weight: 1 }); } } return { nodes, edges }; } /** * Get notes related to a specific note */ getRelatedNotes(notePath: string, maxDepth: number = 2): Note[] { const related = new Set<string>(); const visited = new Set<string>(); const traverse = (path: string, depth: number) => { if (depth > maxDepth || visited.has(path)) { return; } visited.add(path); const note = this.notes.get(path); if (!note) return; if (depth > 0) { related.add(path); } // Follow outgoing links if (note.links) { for (const link of note.links) { const target = this.resolveNotePath(link.target); traverse(target, depth + 1); } } // Follow backlinks if (note.backlinks) { for (const backlink of note.backlinks) { traverse(backlink.source, depth + 1); } } }; traverse(notePath, 0); return Array.from(related) .map(path => this.notes.get(path)) .filter((note): note is Note => note !== undefined); } /** * Find the shortest path between two notes */ findPath(fromPath: string, toPath: string): string[] | null { const queue: Array<{ path: string; route: string[] }> = [{ path: fromPath, route: [fromPath] }]; const visited = new Set<string>(); while (queue.length > 0) { const { path, route } = queue.shift()!; if (path === toPath) { return route; } if (visited.has(path)) { continue; } visited.add(path); const note = this.notes.get(path); if (!note) continue; // Add linked notes to queue if (note.links) { for (const link of note.links) { const target = this.resolveNotePath(link.target); if (!visited.has(target)) { queue.push({ path: target, route: [...route, target] }); } } } } return null; } /** * Analyze the knowledge graph */ analyzeGraph(): GraphAnalysis { const connectionCounts = new Map<string, number>(); const orphans: string[] = []; for (const note of this.notes.values()) { const linkCount = (note.links?.length || 0) + (note.backlinks?.length || 0); connectionCounts.set(note.path, linkCount); if (linkCount === 0) { orphans.push(note.path); } } // Sort by connection count const mostConnected = Array.from(connectionCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([id, connections]) => ({ id, connections })); const graph = this.buildGraph(); return { totalNodes: graph.nodes.filter(n => n.type === 'note').length, totalEdges: graph.edges.length, mostConnectedNodes: mostConnected, orphanNotes: orphans }; } /** * Get notes by tag */ getNotesByTag(tag: string): Note[] { return Array.from(this.notes.values()) .filter(note => note.tags?.includes(tag)); } /** * Get notes in folder */ getNotesByFolder(folder: string): Note[] { return Array.from(this.notes.values()) .filter(note => note.path.startsWith(folder + '/') || this.getFolder(note.path) === folder); } /** * Get all unique tags */ getAllTags(): Array<{ tag: string; count: number }> { const tagCounts = new Map<string, number>(); for (const note of this.notes.values()) { if (note.tags) { for (const tag of note.tags) { tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1); } } } return Array.from(tagCounts.entries()) .map(([tag, count]) => ({ tag, count })) .sort((a, b) => b.count - a.count); } /** * Suggest related notes based on content similarity */ suggestRelatedNotes(notePath: string, limit: number = 5): Array<{ path: string; score: number }> { const note = this.notes.get(notePath); if (!note) return []; const scores = new Map<string, number>(); // Score based on shared tags if (note.tags) { for (const otherNote of this.notes.values()) { if (otherNote.path === notePath) continue; if (otherNote.tags) { const sharedTags = note.tags.filter(tag => otherNote.tags!.includes(tag)); if (sharedTags.length > 0) { scores.set(otherNote.path, (scores.get(otherNote.path) || 0) + sharedTags.length * 10); } } } } // Score based on common words in title const noteWords = new Set(note.title.toLowerCase().split(/\s+/)); for (const otherNote of this.notes.values()) { if (otherNote.path === notePath) continue; const otherWords = otherNote.title.toLowerCase().split(/\s+/); const commonWords = otherWords.filter(word => noteWords.has(word) && word.length > 3); if (commonWords.length > 0) { scores.set(otherNote.path, (scores.get(otherNote.path) || 0) + commonWords.length * 5); } } // Score based on folder proximity const folder = this.getFolder(notePath); for (const otherNote of this.notes.values()) { if (otherNote.path === notePath) continue; if (this.getFolder(otherNote.path) === folder) { scores.set(otherNote.path, (scores.get(otherNote.path) || 0) + 3); } } return Array.from(scores.entries()) .map(([path, score]) => ({ path, score })) .sort((a, b) => b.score - a.score) .slice(0, limit); } // Helper methods private resolveNotePath(linkTarget: string): string { // Remove .md extension if present const normalized = linkTarget.endsWith('.md') ? linkTarget : `${linkTarget}.md`; // Check if exact match exists if (this.notes.has(normalized)) { return normalized; } // Try to find by title match for (const [path, note] of this.notes.entries()) { if (note.title === linkTarget || path.endsWith(`/${normalized}`)) { return path; } } return normalized; } private getFolder(path: string): string { const lastSlash = path.lastIndexOf('/'); return lastSlash === -1 ? '.' : path.substring(0, lastSlash); } }

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/bazylhorsey/obsidian-mcp-server'

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