Skip to main content
Glama

Obsidian MCP Server

Apache 2.0
338
222
  • Apple
  • Linux
logic.ts11.1 kB
import path from "node:path"; // node:path provides OS-specific path functions; using path.posix for vault path manipulation. import { z } from "zod"; import { ObsidianRestApiService, VaultCacheService, } from "../../../services/obsidianRestAPI/index.js"; import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; import { logger, RequestContext, retryWithDelay, } from "../../../utils/index.js"; // ==================================================================================== // Schema Definitions for Input Validation // ==================================================================================== /** * Zod schema for validating the input parameters of the 'obsidian_delete_note' tool. */ export const ObsidianDeleteNoteInputSchema = z .object({ /** * The vault-relative path to the file to be permanently deleted. * Must include the file extension (e.g., "Old Notes/Obsolete File.md"). * The tool first attempts a case-sensitive match. If not found, it attempts * a case-insensitive fallback search within the same directory. */ filePath: z .string() .min(1, "filePath cannot be empty") .describe( 'The vault-relative path to the file to be deleted (e.g., "archive/old-file.md"). Tries case-sensitive first, then case-insensitive fallback.', ), }) .describe( "Input parameters for permanently deleting a specific file within the connected Obsidian vault. Includes a case-insensitive path fallback.", ); /** * TypeScript type inferred from the input schema (`ObsidianDeleteNoteInputSchema`). * Represents the validated input parameters used within the core processing logic. */ export type ObsidianDeleteNoteInput = z.infer< typeof ObsidianDeleteNoteInputSchema >; // ==================================================================================== // Response Type Definition // ==================================================================================== /** * Defines the structure of the successful response returned by the `processObsidianDeleteNote` function. * This object is typically serialized to JSON and sent back to the client. */ export interface ObsidianDeleteNoteResponse { /** Indicates whether the deletion operation was successful. */ success: boolean; /** A human-readable message confirming the deletion and specifying the path used. */ message: string; } // ==================================================================================== // Core Logic Function // ==================================================================================== /** * Processes the core logic for deleting a file from the Obsidian vault. * * It attempts to delete the file using the provided path (case-sensitive first). * If that fails with a 'NOT_FOUND' error, it attempts a case-insensitive fallback: * it lists the directory, finds a unique case-insensitive match for the filename, * and retries the deletion with the corrected path. * * @param {ObsidianDeleteNoteInput} params - The validated input parameters. * @param {RequestContext} context - The request context for logging and correlation. * @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service. * @returns {Promise<ObsidianDeleteNoteResponse>} A promise resolving to the structured success response * containing a confirmation message. * @throws {McpError} Throws an McpError if the file cannot be found (even with fallback), * if there's an ambiguous fallback match, or if any other API interaction fails. */ export const processObsidianDeleteNote = async ( params: ObsidianDeleteNoteInput, context: RequestContext, obsidianService: ObsidianRestApiService, vaultCacheService: VaultCacheService | undefined, ): Promise<ObsidianDeleteNoteResponse> => { const { filePath: originalFilePath } = params; let effectiveFilePath = originalFilePath; // Track the path actually used for deletion logger.debug( `Processing obsidian_delete_note request for path: ${originalFilePath}`, context, ); const shouldRetryNotFound = (err: unknown) => err instanceof McpError && err.code === BaseErrorCode.NOT_FOUND; try { // --- Attempt 1: Delete using the provided path (case-sensitive) --- const deleteContext = { ...context, operation: "deleteFileAttempt", caseSensitive: true, }; logger.debug( `Attempting to delete file (case-sensitive): ${originalFilePath}`, deleteContext, ); await retryWithDelay( () => obsidianService.deleteFile(originalFilePath, deleteContext), { operationName: "deleteFile", context: deleteContext, maxRetries: 3, delayMs: 300, shouldRetry: shouldRetryNotFound, }, ); // If the above call succeeds, the file was deleted using the exact path. logger.debug( `Successfully deleted file using exact path: ${originalFilePath}`, deleteContext, ); if (vaultCacheService) { await vaultCacheService.updateCacheForFile( originalFilePath, deleteContext, ); } return { success: true, message: `File '${originalFilePath}' deleted successfully.`, }; } catch (error) { // --- Attempt 2: Case-insensitive fallback if initial delete failed with NOT_FOUND --- if (error instanceof McpError && error.code === BaseErrorCode.NOT_FOUND) { logger.info( `File not found with exact path: ${originalFilePath}. Attempting case-insensitive fallback for deletion.`, context, ); const fallbackContext = { ...context, operation: "deleteFileFallback" }; try { // Use POSIX path functions for vault path manipulation const dirname = path.posix.dirname(originalFilePath); const filenameLower = path.posix .basename(originalFilePath) .toLowerCase(); // Handle case where the file is in the vault root (dirname is '.') const dirToList = dirname === "." ? "/" : dirname; logger.debug( `Listing directory for fallback deletion: ${dirToList}`, fallbackContext, ); const filesInDir = await retryWithDelay( () => obsidianService.listFiles(dirToList, fallbackContext), { operationName: "listFilesForDeleteFallback", context: fallbackContext, maxRetries: 3, delayMs: 300, shouldRetry: shouldRetryNotFound, }, ); // Filter directory listing for files matching the lowercase filename const matches = filesInDir.filter( (f) => !f.endsWith("/") && // Ensure it's a file path.posix.basename(f).toLowerCase() === filenameLower, ); if (matches.length === 1) { // Found exactly one case-insensitive match const correctFilename = path.posix.basename(matches[0]); effectiveFilePath = path.posix.join(dirname, correctFilename); // Update the path to use logger.info( `Found case-insensitive match: ${effectiveFilePath}. Retrying delete.`, fallbackContext, ); // Retry deleting with the correctly cased path const retryContext = { ...fallbackContext, subOperation: "retryDelete", effectiveFilePath, }; await retryWithDelay( () => obsidianService.deleteFile(effectiveFilePath, retryContext), { operationName: "deleteFileFallback", context: retryContext, maxRetries: 3, delayMs: 300, shouldRetry: shouldRetryNotFound, }, ); logger.debug( `Successfully deleted file using fallback path: ${effectiveFilePath}`, retryContext, ); if (vaultCacheService) { await vaultCacheService.updateCacheForFile( effectiveFilePath, retryContext, ); } return { success: true, message: `File '${effectiveFilePath}' (found via case-insensitive match for '${originalFilePath}') deleted successfully.`, }; } else if (matches.length > 1) { // Ambiguous match: Multiple files match case-insensitively const errorMsg = `Deletion failed: Ambiguous case-insensitive matches for '${originalFilePath}'. Found: [${matches.join(", ")}]. Cannot determine which file to delete.`; logger.error(errorMsg, { ...fallbackContext, matches }); // Use CONFLICT code for ambiguity, as NOT_FOUND isn't quite right anymore. throw new McpError(BaseErrorCode.CONFLICT, errorMsg, fallbackContext); } else { // No match found even with fallback const errorMsg = `Deletion failed: File not found for '${originalFilePath}' (case-insensitive fallback also failed).`; logger.error(errorMsg, fallbackContext); // Stick with NOT_FOUND as the original error reason holds. throw new McpError( BaseErrorCode.NOT_FOUND, errorMsg, fallbackContext, ); } } catch (fallbackError) { // Catch errors specifically from the fallback logic (e.g., listFiles error, retry delete error) if (fallbackError instanceof McpError) { // Log and re-throw known errors from fallback logger.error( `McpError during fallback deletion for ${originalFilePath}: ${fallbackError.message}`, fallbackError, fallbackContext, ); throw fallbackError; } else { // Wrap unexpected fallback errors const errorMessage = `Unexpected error during case-insensitive fallback deletion for ${originalFilePath}`; logger.error( errorMessage, fallbackError instanceof Error ? fallbackError : undefined, fallbackContext, ); throw new McpError( BaseErrorCode.INTERNAL_ERROR, `${errorMessage}: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`, fallbackContext, ); } } } else { // Re-throw errors from the initial delete attempt that were not NOT_FOUND or McpError if (error instanceof McpError) { logger.error( `McpError during initial delete attempt for ${originalFilePath}: ${error.message}`, error, context, ); throw error; } else { const errorMessage = `Unexpected error deleting Obsidian file ${originalFilePath}`; logger.error( errorMessage, error instanceof Error ? error : undefined, context, ); throw new McpError( BaseErrorCode.INTERNAL_ERROR, `${errorMessage}: ${error instanceof Error ? error.message : String(error)}`, context, ); } } } };

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

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