Skip to main content
Glama

Obsidian MCP Server - Enhanced

by BoweyLou
logic.ts•12.2 kB
/** * @fileoverview Logic for managing Obsidian block references and heading operations. */ import { RequestContext } from "../../../utils/index.js"; import { ObsidianRestApiService } from "../../../services/obsidianRestAPI/index.js"; import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; export interface BlockReferenceOperation { operation: "insert_under_heading" | "create_block_reference" | "get_heading_content" | "list_headings" | "get_block_content" | "append_to_heading" | "prepend_to_heading"; filePath: string; heading?: string; headingLevel?: number; content?: string; blockId?: string; position?: "start" | "end" | "after_heading" | "before_next_heading"; createHeading?: boolean; includeSubheadings?: boolean; } export interface BlockReferenceResult { success: boolean; operation: string; filePath: string; heading?: string; blockId?: string; content?: string; headings?: Array<{ level: number; text: string; line: number }>; created?: boolean; inserted?: boolean; message?: string; } /** * Executes block reference and heading operations. */ export async function executeBlockReferenceOperation( operation: BlockReferenceOperation, obsidianService: ObsidianRestApiService, context: RequestContext, ): Promise<BlockReferenceResult> { const { operation: op, filePath, heading, headingLevel, content, blockId, position = "end", createHeading = false, includeSubheadings = false } = operation; try { // Validate operation-specific requirements if (["insert_under_heading", "get_heading_content", "append_to_heading", "prepend_to_heading"].includes(op) && !heading) { throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Heading is required for ${op} operation`); } if (["insert_under_heading", "append_to_heading", "prepend_to_heading", "create_block_reference"].includes(op) && !content) { throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Content is required for ${op} operation`); } if (["get_block_content", "create_block_reference"].includes(op) && !blockId) { throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Block ID is required for ${op} operation`); } switch (op) { case "list_headings": return await listHeadings(filePath, obsidianService, context); case "get_heading_content": return await getHeadingContent(filePath, heading!, includeSubheadings, obsidianService, context); case "get_block_content": return await getBlockContent(filePath, blockId!, obsidianService, context); case "insert_under_heading": return await insertUnderHeading(filePath, heading!, content!, position, createHeading, obsidianService, context, headingLevel); case "append_to_heading": return await appendToHeading(filePath, heading!, content!, createHeading, obsidianService, context, headingLevel); case "prepend_to_heading": return await prependToHeading(filePath, heading!, content!, createHeading, obsidianService, context, headingLevel); case "create_block_reference": return await createBlockReference(filePath, content!, blockId!, obsidianService, context); default: throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Unknown operation: ${op}`); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Block reference operation failed: ${error instanceof Error ? error.message : String(error)}`); } } /** * List all headings in a file. */ async function listHeadings( filePath: string, obsidianService: ObsidianRestApiService, context: RequestContext, ): Promise<BlockReferenceResult> { const fileContent = await obsidianService.getFileContent(filePath, "markdown", context) as string; const lines = fileContent.split('\n'); const headings: Array<{ level: number; text: string; line: number }> = []; lines.forEach((line, index) => { const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch) { headings.push({ level: headingMatch[1].length, text: headingMatch[2].trim(), line: index + 1, }); } }); return { success: true, operation: "list_headings", filePath, headings, message: `Found ${headings.length} headings`, }; } /** * Get content under a specific heading. */ async function getHeadingContent( filePath: string, heading: string, includeSubheadings: boolean, obsidianService: ObsidianRestApiService, context: RequestContext, ): Promise<BlockReferenceResult> { const fileContent = await obsidianService.getFileContent(filePath, "markdown", context) as string; const lines = fileContent.split('\n'); const headingInfo = findHeading(lines, heading); if (!headingInfo) { throw new McpError(BaseErrorCode.NOT_FOUND, `Heading "${heading}" not found in file`); } const content = extractHeadingContent(lines, headingInfo.line, headingInfo.level, includeSubheadings); return { success: true, operation: "get_heading_content", filePath, heading, content, message: `Retrieved content under heading "${heading}"`, }; } /** * Get content of a specific block reference. */ async function getBlockContent( filePath: string, blockId: string, obsidianService: ObsidianRestApiService, context: RequestContext, ): Promise<BlockReferenceResult> { const fileContent = await obsidianService.getFileContent(filePath, "markdown", context) as string; const lines = fileContent.split('\n'); const blockLine = lines.findIndex(line => line.includes(`^${blockId}`)); if (blockLine === -1) { throw new McpError(BaseErrorCode.NOT_FOUND, `Block reference "^${blockId}" not found in file`); } // Get the content of the line containing the block reference const content = lines[blockLine].replace(/\s*\^[\w-]+\s*$/, '').trim(); return { success: true, operation: "get_block_content", filePath, blockId, content, message: `Retrieved content for block "^${blockId}"`, }; } /** * Insert content under a specific heading. */ async function insertUnderHeading( filePath: string, heading: string, content: string, position: string, createHeading: boolean, obsidianService: ObsidianRestApiService, context: RequestContext, headingLevel?: number, ): Promise<BlockReferenceResult> { const fileContent = await obsidianService.getFileContent(filePath, "markdown", context) as string; const lines = fileContent.split('\n'); let headingInfo = findHeading(lines, heading); let created = false; if (!headingInfo && createHeading) { // Create the heading at the end of the file const level = headingLevel || 2; const headingText = `${'#'.repeat(level)} ${heading}`; lines.push('', headingText, ''); headingInfo = { line: lines.length - 2, level, text: heading }; created = true; } else if (!headingInfo) { throw new McpError(BaseErrorCode.NOT_FOUND, `Heading "${heading}" not found. Set createHeading=true to create it.`); } const insertionPoint = getInsertionPoint(lines, headingInfo!, position); lines.splice(insertionPoint, 0, content); const newContent = lines.join('\n'); await obsidianService.updateFileContent(filePath, newContent, context); return { success: true, operation: "insert_under_heading", filePath, heading, created, inserted: true, message: `Inserted content under heading "${heading}"${created ? " (created heading)" : ""}`, }; } /** * Append content to a heading section. */ async function appendToHeading( filePath: string, heading: string, content: string, createHeading: boolean, obsidianService: ObsidianRestApiService, context: RequestContext, headingLevel?: number, ): Promise<BlockReferenceResult> { return await insertUnderHeading(filePath, heading, content, "before_next_heading", createHeading, obsidianService, context, headingLevel); } /** * Prepend content to a heading section. */ async function prependToHeading( filePath: string, heading: string, content: string, createHeading: boolean, obsidianService: ObsidianRestApiService, context: RequestContext, headingLevel?: number, ): Promise<BlockReferenceResult> { return await insertUnderHeading(filePath, heading, content, "after_heading", createHeading, obsidianService, context, headingLevel); } /** * Create a block reference for specific content. */ async function createBlockReference( filePath: string, content: string, blockId: string, obsidianService: ObsidianRestApiService, context: RequestContext, ): Promise<BlockReferenceResult> { const fileContent = await obsidianService.getFileContent(filePath, "markdown", context) as string; const lines = fileContent.split('\n'); // Find if block ID already exists const existingBlock = lines.findIndex(line => line.includes(`^${blockId}`)); if (existingBlock !== -1) { throw new McpError(BaseErrorCode.CONFLICT, `Block reference "^${blockId}" already exists in file`); } // Add the content with block reference const contentWithBlock = `${content} ^${blockId}`; lines.push('', contentWithBlock); const newContent = lines.join('\n'); await obsidianService.updateFileContent(filePath, newContent, context); return { success: true, operation: "create_block_reference", filePath, blockId, content, inserted: true, message: `Created block reference "^${blockId}" with content`, }; } /** * Helper function to find a heading in lines. */ function findHeading(lines: string[], targetHeading: string): { line: number; level: number; text: string } | null { for (let i = 0; i < lines.length; i++) { const line = lines[i]; const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch) { const headingText = headingMatch[2].trim(); if (headingText.toLowerCase() === targetHeading.toLowerCase()) { return { line: i, level: headingMatch[1].length, text: headingText, }; } } } return null; } /** * Extract content under a heading. */ function extractHeadingContent(lines: string[], headingLine: number, headingLevel: number, includeSubheadings: boolean): string { const contentLines: string[] = []; for (let i = headingLine + 1; i < lines.length; i++) { const line = lines[i]; const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch) { const currentLevel = headingMatch[1].length; if (currentLevel <= headingLevel) { // Found a heading at the same level or higher, stop here break; } if (!includeSubheadings && currentLevel > headingLevel) { // Skip subheadings if not including them continue; } } contentLines.push(line); } return contentLines.join('\n').trim(); } /** * Get the insertion point for content based on position. */ function getInsertionPoint(lines: string[], headingInfo: { line: number; level: number }, position: string): number { switch (position) { case "after_heading": return headingInfo.line + 1; case "before_next_heading": // Find the next heading at the same level or higher for (let i = headingInfo.line + 1; i < lines.length; i++) { const line = lines[i]; const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch && headingMatch[1].length <= headingInfo.level) { return i; } } return lines.length; // Insert at end if no next heading found case "start": return headingInfo.line + 1; case "end": default: // Find the end of this heading's section for (let i = headingInfo.line + 1; i < lines.length; i++) { const line = lines[i]; const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch && headingMatch[1].length <= headingInfo.level) { return i; } } return lines.length; // Insert at end if no next heading found } }

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