UNPKG

obsidian-mcp-server

Version:

Model Context Protocol (MCP) server designed for LLMs to interact with Obsidian vaults. Provides secure, token-aware tools for seamless knowledge base management through a standardized interface.

172 lines 6.28 kB
import { PropertyManager } from "../tools/properties/manager.js"; import { sep } from "path"; import { createLogger, ErrorCategoryType } from "../utils/logging.js"; // Create a logger for tag resources const logger = createLogger('TagResource'); /** * Helper function to safely convert any error to an object */ function errorToObject(error) { if (error instanceof Error) { return { name: error.name, message: error.message, stack: error.stack, errorCategory: ErrorCategoryType.CATEGORY_SYSTEM }; } else if (typeof error === 'object' && error !== null) { return error; } else { return { error: String(error), errorCategory: ErrorCategoryType.CATEGORY_UNKNOWN }; } } /** * Resource for providing tags used in the Obsidian vault */ export class TagResource { client; tagCache = new Map(); propertyManager; isInitialized = false; lastUpdate = 0; updateInterval = 5000; // 5 seconds constructor(client) { this.client = client; this.propertyManager = new PropertyManager(client); this.initializeCache(); } /** * Get resource description for the MCP server */ getResourceDescription() { return { uri: "obsidian://tags", name: "Obsidian Tags", description: "List of all tags used across the Obsidian vault with their usage counts", mimeType: "application/json" }; } /** * Initialize the tag cache */ async initializeCache() { logger.startTimer('init_tag_cache'); try { logger.info('Initializing tag cache'); // Get all markdown files using platform-agnostic path pattern const query = { "glob": [`**${sep}*.md`.replace(/\\/g, '/'), { "var": "path" }] }; const results = await this.client.searchJson(query); this.tagCache.clear(); // Process each file for (const result of results) { if (!('filename' in result)) continue; try { const content = await this.client.getFileContents(result.filename); // Only extract tags from frontmatter YAML const properties = this.propertyManager.parseProperties(content); if (properties.tags) { properties.tags.forEach((tag) => { this.addTag(tag, result.filename); }); } } catch (error) { logger.error(`Failed to process file ${result.filename}:`, errorToObject(error)); } } this.isInitialized = true; this.lastUpdate = Date.now(); const elapsedMs = logger.endTimer('init_tag_cache'); logger.logOperationResult(true, 'initialize_tag_cache', elapsedMs, { tagCount: this.tagCache.size, fileCount: results.length }); logger.info(`Tag cache initialized with ${this.tagCache.size} unique tags`); } catch (error) { const elapsedMs = logger.endTimer('init_tag_cache'); logger.logOperationResult(false, 'initialize_tag_cache', elapsedMs); logger.error("Failed to initialize tag cache:", errorToObject(error)); throw error; } } /** * Add a tag to the cache */ addTag(tag, filepath) { if (!this.tagCache.has(tag)) { this.tagCache.set(tag, new Set()); } this.tagCache.get(tag).add(filepath); } /** * Update the cache if needed */ async updateCacheIfNeeded() { const now = Date.now(); if (now - this.lastUpdate > this.updateInterval) { logger.debug('Tag cache needs update, refreshing...'); await this.initializeCache(); } } /** * Get the content for the resource */ async getContent() { logger.startTimer('get_tags_content'); try { if (!this.isInitialized) { logger.info('Tag cache not initialized, initializing now'); await this.initializeCache(); } else { await this.updateCacheIfNeeded(); } const response = { tags: Array.from(this.tagCache.entries()) .map(([name, files]) => ({ name, count: files.size, files: Array.from(files).sort() })) .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)), metadata: { totalOccurrences: Array.from(this.tagCache.values()) .reduce((sum, files) => sum + files.size, 0), uniqueTags: this.tagCache.size, scannedFiles: new Set(Array.from(this.tagCache.values()) .flatMap(files => Array.from(files))).size, lastUpdate: this.lastUpdate } }; logger.debug(`Returning tag resource with ${response.tags.length} tags`); const elapsedMs = logger.endTimer('get_tags_content'); logger.logOperationResult(true, 'get_tags', elapsedMs, { tagCount: response.tags.length, totalOccurrences: response.metadata.totalOccurrences, uniqueTags: response.metadata.uniqueTags, scannedFiles: response.metadata.scannedFiles }); return [{ type: "text", text: JSON.stringify(response, null, 2), uri: this.getResourceDescription().uri }]; } catch (error) { const elapsedMs = logger.endTimer('get_tags_content'); logger.logOperationResult(false, 'get_tags', elapsedMs); logger.error("Failed to get tags:", errorToObject(error)); throw error; } } } //# sourceMappingURL=tags.js.map