Skip to main content
Glama

update_note

Modify existing notes in Obsidian by applying text replacements or inserting content at specific headings or positions—ideal for precise edits.

Instructions

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

Input Schema

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

Implementation Reference

  • Primary handler for 'update_note' tool. Processes edit operations, attempts Obsidian API inserts, falls back to filesystem edits via applyNoteEdits.
    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, }, ], }; }
  • Core implementation logic for applying note edits. Handles both replace (text search/replace) and insert (heading/block-targeted) operations with Markdown parsing and 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`; }
  • src/index.ts:1219-1291 (registration)
    Registration of 'update_note' tool in the tools list returned by ListToolsRequestHandler.
    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'], }, },
  • Input schema defining parameters for update_note: path, array of edits (replace or insert modes), and optional dryRun.
    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'],
  • Helper function to parse Markdown structure, enabling heading and block ID targeting for insert operations.
    function parseMarkdown(content: string): MarkdownElement[] { const lines = content.split('\n'); const elements: MarkdownElement[] = []; let currentElement: MarkdownElement | null = null; let inCodeBlock = false; let codeBlockFence = ''; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmedLine = line.trim(); // 检查代码块 if (trimmedLine.startsWith('```') || trimmedLine.startsWith('~~~')) { if (!inCodeBlock) { // 开始代码块 inCodeBlock = true; codeBlockFence = trimmedLine.substring(0, 3); currentElement = { type: 'code', content: line, startLine: i, endLine: i }; } else if (trimmedLine.startsWith(codeBlockFence)) { // 结束代码块 inCodeBlock = false; if (currentElement) { currentElement.content += '\n' + line; currentElement.endLine = i; elements.push(currentElement); currentElement = null; } } else if (currentElement) { // 代码块内容 currentElement.content += '\n' + line; } continue; } // 在代码块内,跳过其他处理 if (inCodeBlock) { if (currentElement) { currentElement.content += '\n' + line; } continue; } // 检查标题 const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); if (headingMatch) { // 完成之前的元素 if (currentElement) { currentElement.endLine = i - 1; elements.push(currentElement); } // 检查块ID const blockIdMatch = headingMatch[2].match(/^(.+?)\s*\^([a-zA-Z0-9-_]+)$/); const headingText = blockIdMatch ? blockIdMatch[1].trim() : headingMatch[2].trim(); const blockId = blockIdMatch ? blockIdMatch[2] : undefined; currentElement = { type: 'heading', level: headingMatch[1].length, content: headingText, startLine: i, endLine: i, blockId: blockId }; elements.push(currentElement); currentElement = null; continue; } // 检查块ID(独立的块ID) const blockIdMatch = line.match(/^\s*\^([a-zA-Z0-9-_]+)\s*$/); if (blockIdMatch) { // 如果前面有段落,给它添加块ID if (elements.length > 0) { const lastElement = elements[elements.length - 1]; if (lastElement.type === 'paragraph' && !lastElement.blockId) { lastElement.blockId = blockIdMatch[1]; lastElement.endLine = i; } } continue; } // 检查列表项 const listMatch = line.match(/^(\s*)([-*+]|\d+\.)\s+(.+)$/); if (listMatch) { if (currentElement?.type !== 'list') { // 完成之前的元素 if (currentElement) { currentElement.endLine = i - 1; elements.push(currentElement); } // 开始新的列表 currentElement = { type: 'list', content: line, startLine: i, endLine: i }; } else { // 继续列表 currentElement.content += '\n' + line; currentElement.endLine = i; } continue; } // 空行处理 if (trimmedLine === '') { if (currentElement) { currentElement.endLine = i - 1; elements.push(currentElement); currentElement = null; } continue; } // 段落处理 if (currentElement?.type === 'paragraph') { currentElement.content += '\n' + line; currentElement.endLine = i; } else { // 完成之前的元素 if (currentElement) { currentElement.endLine = i - 1; elements.push(currentElement); } // 开始新段落 currentElement = { type: 'paragraph', content: line, startLine: i, endLine: i }; } } // 完成最后的元素 if (currentElement) { currentElement.endLine = lines.length - 1; elements.push(currentElement); } return elements; }

Other Tools

Related Tools

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