Skip to main content
Glama

update_note

Modify existing notes in Obsidian by replacing specific text or inserting content at precise locations like headings or blocks.

Instructions

Update content in an existing note using text replacements or precise insertions

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
pathYesPath to the note within the vault
editsYesArray of edit operations to apply
dryRunNoPreview changes without applying them

Implementation Reference

  • Main handler function for the update_note tool. Processes array of edits on a note, prefers Obsidian API for inserts, falls back to filesystem edits via applyNoteEdits. Supports dry-run previews.
    private async handleUpdateNote(args: any) { if (!args?.path || !args?.edits) { throw new Error('Path and edits are required'); } if (!Array.isArray(args.edits)) { throw new Error('Edits must be an array'); } const dryRun = args.dryRun || false; const notePath = args.path; const edits = args.edits; // 尝试使用 Obsidian API 进行插入操作,如果失败则回退到文件系统 let apiResults: string[] = []; let fallbackEdits: EditOperation[] = []; for (const edit of edits) { const mode = edit.mode || 'replace'; if (mode === 'insert' && !dryRun) { try { // 尝试使用 Obsidian API if (edit.heading) { await this.patchNoteViaAPI(notePath, edit.heading, edit.content || '', edit.position || 'after'); apiResults.push(`API: Inserted content ${edit.position || 'after'} heading "${edit.heading}"`); } else if (edit.blockId) { await this.patchNoteViaBlockAPI(notePath, edit.blockId, edit.content || '', edit.position || 'after'); apiResults.push(`API: Inserted content ${edit.position || 'after'} block "${edit.blockId}"`); } else { // 无效的插入操作,添加到回退列表 fallbackEdits.push(edit); } } catch (error) { // API 失败,添加到回退列表 console.warn(`API PATCH failed for edit, falling back to filesystem: ${error}`); fallbackEdits.push(edit); } } else { // 替换模式或干运行,添加到回退列表 fallbackEdits.push(edit); } } // 如果有回退操作,使用文件系统方法 let filesystemResult = ''; if (fallbackEdits.length > 0) { filesystemResult = await applyNoteEdits(notePath, fallbackEdits, dryRun); } // 合并结果 let result = ''; if (apiResults.length > 0) { result += 'Obsidian API operations:\n' + apiResults.join('\n') + '\n\n'; } if (filesystemResult) { result += fallbackEdits.length === edits.length ? filesystemResult : `Filesystem operations:\n${filesystemResult}`; } if (!result) { result = `File ${notePath} updated successfully`; } return { content: [ { type: 'text', text: result, }, ], }; }
  • src/index.ts:1219-1291 (registration)
    Tool registration in list_tools handler, including name, description, and full inputSchema defining parameters for path, edits array (replace/insert modes), and dryRun option.
    name: 'update_note', description: 'Update content in an existing note using text replacements or precise insertions', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Path to the note within the vault' }, edits: { type: 'array', description: 'Array of edit operations to apply', items: { type: 'object', properties: { // 替换模式 (向后兼容) oldText: { type: 'string', description: 'Text to search for and replace (for replace mode)' }, newText: { type: 'string', description: 'Text to replace with (for replace mode)' }, // 插入模式 mode: { type: 'string', enum: ['replace', 'insert'], description: 'Edit mode: replace (default) or insert', default: 'replace' }, heading: { type: 'string', description: 'Target heading for insert mode' }, content: { type: 'string', description: 'Content to insert (for insert mode)' }, position: { type: 'string', enum: ['before', 'after', 'append', 'prepend'], description: 'Where to insert relative to heading: before (above heading), after (below heading), append (end of section), prepend (start of section)', default: 'after' }, level: { type: 'number', minimum: 1, maximum: 6, description: 'Heading level (1-6) for more precise targeting' }, blockId: { type: 'string', description: 'Block ID for block-based insertion (^block-id)' } }, anyOf: [ { required: ['oldText', 'newText'] }, // 替换模式 { required: ['mode', 'heading', 'content'] }, // 标题插入 { required: ['mode', 'blockId', 'content'] } // 块插入 ] } }, dryRun: { type: 'boolean', description: 'Preview changes without applying them', default: false } }, required: ['path', 'edits'], }, },
  • Primary helper implementing the file editing logic. Reads note, parses Markdown structure, applies sequential replace/insert edits with validation and fuzzy matching, supports dry-run diffs, atomic writes.
    async function applyNoteEdits(filePath: string, edits: EditOperation[], dryRun: boolean = false): Promise<string> { const fullPath = path.join(VAULT_PATH, filePath); // Read current file content let content: string; try { content = fs.readFileSync(fullPath, 'utf-8'); } catch (error) { throw new Error(`Failed to read file ${filePath}: ${error}`); } const originalContent = content; let modifiedContent = normalizeLineEndings(content); // Parse markdown structure for insert operations const elements = parseMarkdown(modifiedContent); let lines = modifiedContent.split('\n'); // Apply edits sequentially for (const edit of edits) { // 向后兼容:如果没有mode字段但有oldText和newText,默认为replace模式 const mode = edit.mode || (edit.oldText && edit.newText ? 'replace' : 'insert'); // 验证编辑操作 const validationErrors = validateEditOperation({ ...edit, mode }); if (validationErrors.length > 0) { throw new Error(`Invalid edit operation: ${validationErrors.join('; ')}`); } if (mode === 'insert') { // Handle insert mode try { lines = handleInsertEdit(lines, elements, edit); modifiedContent = lines.join('\n'); // Re-parse elements after modification for subsequent edits elements.length = 0; elements.push(...parseMarkdown(modifiedContent)); } catch (error) { if (error instanceof InsertError) { throw new Error(`Insert operation failed: ${error.message}`); } throw new Error(`Insert operation failed: ${error instanceof Error ? error.message : String(error)}`); } } else { // Handle replace mode (original logic) const { oldText, newText } = edit; if (!oldText || !newText) { throw new Error('Replace mode requires both oldText and newText'); } if (oldText === newText) { continue; // Skip if no change } // Try exact match first if (modifiedContent.includes(oldText)) { modifiedContent = modifiedContent.replace(oldText, newText); lines = modifiedContent.split('\n'); // Re-parse elements after modification elements.length = 0; elements.push(...parseMarkdown(modifiedContent)); continue; } // Try flexible line-by-line matching const oldLines = oldText.split('\n'); const newLines = newText.split('\n'); let matchFound = false; // Find matching sequence of lines for (let i = 0; i <= lines.length - oldLines.length; i++) { let isMatch = true; const matchedIndentations: string[] = []; // Check if lines match (ignoring leading/trailing whitespace) for (let j = 0; j < oldLines.length; j++) { const contentLine = lines[i + j]; const oldLine = oldLines[j]; // Extract indentation from content line const indentMatch = contentLine.match(/^(\s*)/); const indentation = indentMatch ? indentMatch[1] : ''; matchedIndentations.push(indentation); // Compare trimmed lines if (contentLine.trim() !== oldLine.trim()) { isMatch = false; break; } } if (isMatch) { // Replace the matched lines with new lines, preserving indentation const replacementLines = newLines.map((line, index) => { if (index < matchedIndentations.length) { const originalIndent = matchedIndentations[index]; const lineWithoutIndent = line.replace(/^\s*/, ''); return originalIndent + lineWithoutIndent; } return line; }); // Replace the lines lines.splice(i, oldLines.length, ...replacementLines); modifiedContent = lines.join('\n'); matchFound = true; // Re-parse elements after modification elements.length = 0; elements.push(...parseMarkdown(modifiedContent)); break; } } if (!matchFound) { throw new Error(`Could not find matching text for edit: "${oldText.substring(0, 50)}..."`); } } } if (dryRun) { // Return diff for preview return createUnifiedDiff(originalContent, modifiedContent, filePath); } // Write the modified content atomically const tempFile = fullPath + '.tmp'; try { fs.writeFileSync(tempFile, modifiedContent, 'utf-8'); fs.renameSync(tempFile, fullPath); } catch (error) { // Clean up temp file if it exists if (fs.existsSync(tempFile)) { fs.unlinkSync(tempFile); } throw new Error(`Failed to write file ${filePath}: ${error}`); } return `File ${filePath} updated successfully`; }
  • Helper for insert-mode edits: validates, finds target heading/blockId via parsing, calculates precise position (before/after/append/prepend), inserts content while preserving structure.
    function handleInsertEdit( lines: string[], elements: MarkdownElement[], edit: EditOperation ): string[] { // 验证操作 const validationErrors = validateEditOperation(edit); if (validationErrors.length > 0) { throw new InsertError( validationErrors.join('; '), 'validation', edit.heading || edit.blockId || 'unknown' ); } let targetIndex = -1; let targetElement: MarkdownElement | null = null; try { if (edit.heading) { // 标题插入模式 const headingResult = findHeadingPosition(elements, edit.heading, edit.level); if (!headingResult) { throw new InsertError( `Heading not found: ${edit.heading}${edit.level ? ` (level ${edit.level})` : ''}`, 'heading_search', edit.heading ); } targetIndex = headingResult.index; targetElement = headingResult.element; } else if (edit.blockId) { // 块插入模式 const blockResult = findBlockIdPosition(elements, edit.blockId); if (!blockResult) { throw new InsertError( `Block ID not found: ${edit.blockId}`, 'block_search', edit.blockId ); } targetIndex = blockResult.index; targetElement = blockResult.element; } // 计算插入位置 const insertLine = calculateInsertPosition(elements, targetIndex, edit.position || 'after'); // 插入内容 return insertContentAtLine(lines, insertLine, edit.content || ''); } catch (error) { if (error instanceof InsertError) { throw error; } throw new InsertError( error instanceof Error ? error.message : String(error), 'insert_operation', edit.heading || edit.blockId || 'unknown' ); } }
  • Type definitions for EditOperation (supports replace and insert modes with heading/block targeting) and MarkdownElement (used for structure-aware parsing and positioning).
    interface EditOperation { // 原有的替换模式 oldText?: string; newText?: string; // 新增的插入模式 mode?: 'replace' | 'insert'; heading?: string; // 目标标题 content?: string; // 要插入的内容 position?: 'before' | 'after' | 'append' | 'prepend'; level?: number; // 标题级别 (1-6) blockId?: string; // 块ID引用 } // Markdown 元素结构 interface MarkdownElement { type: 'heading' | 'paragraph' | 'list' | 'code' | 'block'; level?: number; // 标题级别 content: string; // 内容 startLine: number; // 开始行号 endLine: number; // 结束行号 blockId?: string; // 块ID } // 解析 Markdown 内容 function parseMarkdown(content: string): MarkdownElement[] {

Latest Blog Posts

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/newtype-01/obsidian-mcp'

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