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[] {
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full burden. It mentions 'update content' but doesn't disclose critical behavioral traits: whether changes are reversible, permission requirements, error handling (e.g., if note doesn't exist), or side effects. The 'dryRun' parameter hints at preview capability, but this isn't explained in the description. For a mutation tool with zero annotation coverage, this is a significant gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence that front-loads the core action ('Update content in an existing note') and adds method details ('using text replacements or precise insertions'). There's no wasted wording, and it's appropriately sized for the tool's complexity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (mutation with nested edit operations), lack of annotations, and no output schema, the description is incomplete. It doesn't explain return values, error cases, or behavioral nuances like how 'dryRun' works. For a tool that modifies notes with multiple edit modes, more context is needed to guide effective use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema fully documents all parameters. The description adds minimal value beyond the schema by hinting at 'text replacements or precise insertions', which loosely maps to the 'edits' parameter modes. However, it doesn't provide additional syntax, examples, or constraints beyond what's in the schema descriptions. Baseline 3 is appropriate when schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose as 'Update content in an existing note using text replacements or precise insertions', which specifies the verb ('update'), resource ('existing note'), and method ('text replacements or precise insertions'). It distinguishes from siblings like 'create_note' (creates new) and 'delete_note' (removes), but doesn't explicitly differentiate from 'move_note' or 'read_note' beyond the update action.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention prerequisites (e.g., note must exist), compare to 'create_note' for new notes, or specify scenarios where 'replace' vs 'insert' modes are appropriate. Usage is implied by the action but lacks explicit context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other 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