Skip to main content
Glama

Obsidian MCP Server - Enhanced

by BoweyLou
logic.ts•11.4 kB
/** * @fileoverview Core logic for the Obsidian Tasks Query Builder tool. * Provides functionality to build and execute native Tasks plugin queries. * @module obsidianTasksQueryBuilderTool/logic */ import { z } from "zod"; import { ObsidianRestApiService } from "../../../services/obsidianRestAPI/index.js"; import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; import { ErrorHandler, logger, RequestContext, } from "../../../utils/index.js"; /** * Zod schema for validating Tasks Query Builder input parameters */ export const TasksQueryBuilderInputSchema = z.object({ vault: z.string().optional(), // Core query building options query: z.string().optional().describe("Raw Tasks plugin query syntax"), // Alternative: structured query building filters: z.object({ status: z.array(z.enum(["todo", "done", "in-progress", "cancelled", "deferred", "scheduled"])).optional(), priority: z.array(z.enum(["highest", "high", "medium", "low", "lowest"])).optional(), tags: z.array(z.string()).optional(), path: z.string().optional(), due: z.string().optional().describe("Due date filter (e.g., 'today', 'before tomorrow', 'after 2024-01-01')"), scheduled: z.string().optional().describe("Scheduled date filter"), starts: z.string().optional().describe("Start date filter"), created: z.string().optional().describe("Created date filter"), done: z.string().optional().describe("Done date filter"), recurrence: z.boolean().optional().describe("Show only recurring tasks"), hasDescription: z.boolean().optional().describe("Tasks with descriptions"), }).optional(), // Grouping and sorting groupBy: z.array(z.enum([ "status", "priority", "due", "scheduled", "starts", "created", "done", "filename", "folder", "path", "tag", "heading", "urgency", "recurring" ])).optional(), sortBy: z.array(z.object({ field: z.enum([ "status", "priority", "due", "scheduled", "starts", "created", "done", "description", "path", "urgency", "tag" ]), reverse: z.boolean().default(false) })).optional(), // Display options hideOptions: z.object({ editButton: z.boolean().default(false), postponeButton: z.boolean().default(false), backlinks: z.boolean().default(false), priority: z.boolean().default(false), dueDate: z.boolean().default(false), scheduledDate: z.boolean().default(false), startDate: z.boolean().default(false), createdDate: z.boolean().default(false), doneDate: z.boolean().default(false), recurrenceRule: z.boolean().default(false), taskCount: z.boolean().default(false), }).optional(), limit: z.number().int().positive().max(1000).default(100), explain: z.boolean().default(false).describe("Include query explanation"), }); export type TasksQueryBuilderInput = z.infer<typeof TasksQueryBuilderInputSchema>; /** * Response structure for Tasks Query Builder */ export interface TasksQueryBuilderResponse { success: boolean; generatedQuery: string; results?: any[]; explanation?: string; executionTime: string; error?: string; } /** * Build a native Tasks plugin query from structured input */ function buildTasksQuery(input: TasksQueryBuilderInput): string { const queryParts: string[] = []; // If raw query is provided, use it directly if (input.query) { return input.query; } // Build query from structured filters if (input.filters) { const { filters } = input; // Status filters if (filters.status && filters.status.length > 0) { if (filters.status.includes("todo")) { queryParts.push("not done"); } if (filters.status.includes("done")) { queryParts.push("done"); } if (filters.status.includes("in-progress")) { queryParts.push("status.type is IN_PROGRESS"); } if (filters.status.includes("cancelled")) { queryParts.push("status.type is CANCELLED"); } if (filters.status.includes("deferred")) { queryParts.push("status.type is DEFERRED"); } if (filters.status.includes("scheduled")) { queryParts.push("status.type is TODO"); } } // Priority filters if (filters.priority && filters.priority.length > 0) { const priorityConditions = filters.priority.map(p => `priority is ${p}`); queryParts.push(`(${priorityConditions.join(" OR ")})`); } // Tag filters if (filters.tags && filters.tags.length > 0) { const tagConditions = filters.tags.map(tag => `tags include #${tag}`); queryParts.push(`(${tagConditions.join(" OR ")})`); } // Path filter if (filters.path) { queryParts.push(`path includes ${filters.path}`); } // Date filters if (filters.due) { queryParts.push(`due ${filters.due}`); } if (filters.scheduled) { queryParts.push(`scheduled ${filters.scheduled}`); } if (filters.starts) { queryParts.push(`starts ${filters.starts}`); } if (filters.created) { queryParts.push(`created ${filters.created}`); } if (filters.done) { queryParts.push(`done ${filters.done}`); } // Special filters if (filters.recurrence === true) { queryParts.push("is recurring"); } else if (filters.recurrence === false) { queryParts.push("is not recurring"); } if (filters.hasDescription === true) { queryParts.push("has description"); } else if (filters.hasDescription === false) { queryParts.push("no description"); } } // Group by clauses if (input.groupBy && input.groupBy.length > 0) { input.groupBy.forEach(groupField => { queryParts.push(`group by ${groupField}`); }); } // Sort by clauses if (input.sortBy && input.sortBy.length > 0) { input.sortBy.forEach(sort => { const direction = sort.reverse ? " reverse" : ""; queryParts.push(`sort by ${sort.field}${direction}`); }); } // Hide options if (input.hideOptions) { const hideOpts = input.hideOptions; if (hideOpts.editButton) queryParts.push("hide edit button"); if (hideOpts.postponeButton) queryParts.push("hide postpone button"); if (hideOpts.backlinks) queryParts.push("hide backlinks"); if (hideOpts.priority) queryParts.push("hide priority"); if (hideOpts.dueDate) queryParts.push("hide due date"); if (hideOpts.scheduledDate) queryParts.push("hide scheduled date"); if (hideOpts.startDate) queryParts.push("hide start date"); if (hideOpts.createdDate) queryParts.push("hide created date"); if (hideOpts.doneDate) queryParts.push("hide done date"); if (hideOpts.recurrenceRule) queryParts.push("hide recurrence rule"); if (hideOpts.taskCount) queryParts.push("hide task count"); } // Limit if (input.limit && input.limit !== 100) { queryParts.push(`limit ${input.limit}`); } // Explain if (input.explain) { queryParts.push("explain"); } return queryParts.join("\n"); } /** * Execute a Tasks plugin query by embedding it in a note and running a command */ async function executeTasksQuery( query: string, obsidianService: ObsidianRestApiService, context: RequestContext ): Promise<any[]> { try { // First, try to execute the query using Dataview integration // This is a fallback approach since we can't directly access Tasks plugin API const dataviewQuery = ` TABLE WITHOUT ID task.text as Text, task.status.symbol as Status, task.priority.name as Priority, task.due as DueDate, task.scheduled as ScheduledDate, task.start as StartDate, task.done as CompletionDate, task.created as CreatedDate, task.tags as Tags, file.path as FilePath, task.line as LineNumber FROM "/" FLATTEN file.tasks as task WHERE task LIMIT 500`; logger.debug("Executing Tasks query via Dataview fallback", { ...context, query, dataviewQuery, }); const dataviewResults = await obsidianService.searchComplex( dataviewQuery, "application/vnd.olrapi.dataview.dql+txt", context ); // Convert results to a more readable format const results = dataviewResults.map(result => result.result).filter(Boolean); logger.info(`Tasks query executed via Dataview, found ${results.length} tasks`, context); return results; } catch (error) { logger.warning("Failed to execute Tasks query", { ...context, error: error instanceof Error ? error.message : String(error), }); throw error; } } /** * Core logic for building and executing Tasks plugin queries */ export async function obsidianTasksQueryBuilderLogic( input: TasksQueryBuilderInput, context: RequestContext, obsidianService: ObsidianRestApiService, ): Promise<TasksQueryBuilderResponse> { const startTime = Date.now(); logger.info("Building and executing Tasks plugin query", { ...context, operation: "tasksQueryBuilder", hasRawQuery: !!input.query, hasFilters: !!input.filters, hasGroupBy: !!(input.groupBy && input.groupBy.length > 0), hasSortBy: !!(input.sortBy && input.sortBy.length > 0), }); try { // Step 1: Build the Tasks plugin query const generatedQuery = buildTasksQuery(input); if (!generatedQuery.trim()) { throw new McpError( BaseErrorCode.VALIDATION_ERROR, "No valid query could be generated from the provided parameters", { input } ); } logger.debug("Generated Tasks plugin query", { ...context, generatedQuery, }); // Step 2: Execute the query let results: any[] = []; let explanation = ""; let error: string | undefined; try { results = await executeTasksQuery(generatedQuery, obsidianService, context); if (input.explain) { explanation = `This query uses the Tasks plugin syntax to filter and organize tasks. Generated Query: \`\`\` ${generatedQuery} \`\`\` Query Components: ${generatedQuery.split('\n').map(line => `- ${line.trim()}`).join('\n')} Note: This is executed via Dataview integration as a fallback. For full Tasks plugin functionality, embed this query in a note with \`\`\`tasks\` code blocks.`; } } catch (executionError) { error = executionError instanceof Error ? executionError.message : String(executionError); logger.warning("Query execution failed, returning query only", { ...context, error, }); } const executionTime = `${Date.now() - startTime}ms`; logger.info("Tasks query builder completed", { ...context, executionTime, generatedQuery: generatedQuery.substring(0, 100) + "...", resultsCount: results.length, hasError: !!error, }); return { success: !error, generatedQuery, results: results.length > 0 ? results : undefined, explanation, executionTime, error, }; } catch (error) { const executionTime = `${Date.now() - startTime}ms`; logger.error("Tasks query builder failed", { ...context, error: error instanceof Error ? error.message : String(error), executionTime, }); throw new McpError( BaseErrorCode.INTERNAL_ERROR, `Tasks query builder failed: ${error instanceof Error ? error.message : String(error)}`, { executionTime, input }, ); } }

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

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