Skip to main content
Glama

Obsidian MCP Server

by bazylhorsey
DataviewService.ts•9.59 kB
/** * Dataview-style query service for notes */ import type { Note } from '../types/index.js'; import type { DataviewQuery, QueryFilter, QuerySort, DataviewResult, GroupedDataviewResult, QueryExecutionResult, NoteMetadata, InlineField } from '../types/dataview.js'; export class DataviewService { private notes: Note[] = []; /** * Update the notes collection */ updateNotes(notes: Note[]): void { this.notes = notes; } /** * Execute a Dataview query */ async executeQuery(query: DataviewQuery): Promise<QueryExecutionResult> { let results = this.notes.map(note => this.noteToDataviewResult(note)); // Apply FROM clause (source filtering) if (query.from) { results = this.applyFromFilter(results, query.from); } // Apply WHERE clause (filters) if (query.where && query.where.length > 0) { results = this.applyFilters(results, query.where); } // Apply SORT if (query.sort && query.sort.length > 0) { results = this.applySorting(results, query.sort); } // Apply field selection if (query.select && query.select.length > 0) { results = this.applyFieldSelection(results, query.select); } const totalCount = results.length; // Apply GROUP BY if (query.groupBy) { const grouped = this.applyGrouping(results, query.groupBy); return { results: grouped, totalCount, grouped: true }; } // Apply LIMIT if (query.limit && query.limit > 0) { results = results.slice(0, query.limit); } return { results, totalCount, grouped: false }; } /** * Get metadata for a specific note */ getMetadata(notePath: string): NoteMetadata | null { const note = this.notes.find(n => n.path === notePath); if (!note) return null; return this.extractMetadata(note); } /** * Extract all metadata from a note */ extractMetadata(note: Note): NoteMetadata { const metadata: NoteMetadata = { path: note.path, title: note.title, created: note.createdAt, modified: note.modifiedAt, tags: note.tags, aliases: note.frontmatter?.aliases, }; // Add all frontmatter fields if (note.frontmatter) { Object.assign(metadata, note.frontmatter); } // Extract inline fields const inlineFields = this.extractInlineFields(note.content); for (const field of inlineFields) { if (!metadata[field.key]) { metadata[field.key] = this.parseFieldValue(field.value); } } return metadata; } /** * Extract inline fields from content (key:: value pattern) */ extractInlineFields(content: string): InlineField[] { const fields: InlineField[] = []; const lines = content.split('\n'); // Match patterns like: key:: value or [key:: value] const inlineRegex = /(\w+)::\s*(.+?)(?:\s*$|\s*\])/g; lines.forEach((line, index) => { let match; while ((match = inlineRegex.exec(line)) !== null) { fields.push({ key: match[1].trim(), value: match[2].trim(), line: index + 1 }); } }); return fields; } /** * Get all unique values for a metadata field */ getUniqueValues(field: string): any[] { const values = new Set<any>(); for (const note of this.notes) { const metadata = this.extractMetadata(note); const value = this.getNestedValue(metadata, field); if (value !== undefined && value !== null) { if (Array.isArray(value)) { value.forEach(v => values.add(v)); } else { values.add(value); } } } return Array.from(values); } /** * Get notes grouped by a field */ groupNotesByField(field: string): Map<string, Note[]> { const groups = new Map<string, Note[]>(); for (const note of this.notes) { const metadata = this.extractMetadata(note); const value = this.getNestedValue(metadata, field); const key = value !== undefined && value !== null ? String(value) : '(none)'; if (!groups.has(key)) { groups.set(key, []); } groups.get(key)!.push(note); } return groups; } // Private helper methods private noteToDataviewResult(note: Note): DataviewResult { const metadata = this.extractMetadata(note); return { metadata, ...metadata // Flatten metadata to top level for easy access (includes path and title) } as DataviewResult; } private applyFromFilter(results: DataviewResult[], from: string | string[]): DataviewResult[] { const sources = Array.isArray(from) ? from : [from]; return results.filter(result => { return sources.some(source => { if (source.startsWith('#')) { // Tag filter const tag = source.substring(1); return result.metadata.tags?.includes(tag); } else if (source.startsWith('"') && source.endsWith('"')) { // Folder filter const folder = source.slice(1, -1); return result.path.startsWith(folder); } else { // Folder filter without quotes return result.path.startsWith(source); } }); }); } private applyFilters(results: DataviewResult[], filters: QueryFilter[]): DataviewResult[] { return results.filter(result => { return filters.every(filter => this.evaluateFilter(result, filter)); }); } private evaluateFilter(result: DataviewResult, filter: QueryFilter): boolean { const value = this.getNestedValue(result, filter.field); switch (filter.operator) { case 'exists': return value !== undefined && value !== null; case 'eq': return value === filter.value; case 'neq': return value !== filter.value; case 'gt': return value > filter.value; case 'gte': return value >= filter.value; case 'lt': return value < filter.value; case 'lte': return value <= filter.value; case 'contains': if (typeof value === 'string') { return value.includes(String(filter.value)); } if (Array.isArray(value)) { return value.includes(filter.value); } return false; case 'startsWith': return typeof value === 'string' && value.startsWith(String(filter.value)); case 'endsWith': return typeof value === 'string' && value.endsWith(String(filter.value)); default: return false; } } private applySorting(results: DataviewResult[], sorts: QuerySort[]): DataviewResult[] { return results.sort((a, b) => { for (const sort of sorts) { const aVal = this.getNestedValue(a, sort.field); const bVal = this.getNestedValue(b, sort.field); let comparison = 0; if (aVal === undefined || aVal === null) comparison = 1; else if (bVal === undefined || bVal === null) comparison = -1; else if (aVal < bVal) comparison = -1; else if (aVal > bVal) comparison = 1; if (comparison !== 0) { return sort.direction === 'asc' ? comparison : -comparison; } } return 0; }); } private applyFieldSelection(results: DataviewResult[], fields: string[]): DataviewResult[] { return results.map(result => { const selected: DataviewResult = { path: result.path, title: result.title, metadata: {} }; for (const field of fields) { const value = this.getNestedValue(result, field); if (value !== undefined) { this.setNestedValue(selected, field, value); } } return selected; }); } private applyGrouping(results: DataviewResult[], groupByField: string): GroupedDataviewResult[] { const groups = new Map<string, DataviewResult[]>(); for (const result of results) { const groupValue = this.getNestedValue(result, groupByField); const key = groupValue !== undefined && groupValue !== null ? String(groupValue) : '(none)'; if (!groups.has(key)) { groups.set(key, []); } groups.get(key)!.push(result); } return Array.from(groups.entries()).map(([group, items]) => ({ group, items })); } private getNestedValue(obj: any, path: string): any { const keys = path.split('.'); let value = obj; for (const key of keys) { if (value === undefined || value === null) return undefined; value = value[key]; } return value; } private setNestedValue(obj: any, path: string, value: any): void { const keys = path.split('.'); const lastKey = keys.pop()!; let current = obj; for (const key of keys) { if (!current[key]) current[key] = {}; current = current[key]; } current[lastKey] = value; } private parseFieldValue(value: string): any { // Try to parse as number if (/^-?\d+(\.\d+)?$/.test(value)) { return parseFloat(value); } // Try to parse as boolean if (value.toLowerCase() === 'true') return true; if (value.toLowerCase() === 'false') return false; // Try to parse as date const dateMatch = value.match(/^\d{4}-\d{2}-\d{2}$/); if (dateMatch) { return new Date(value); } // Try to parse as array if (value.startsWith('[') && value.endsWith(']')) { try { return JSON.parse(value); } catch { return value; } } // Return as string return value; } }

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