Skip to main content
Glama

obsidian_patch_content

Insert content at specific locations within Obsidian notes using headings, blocks, or frontmatter to append, prepend, or replace text.

Instructions

Insert content at specific locations within notes using headings, blocks, or frontmatter.

CRITICAL: For heading targets, you MUST provide the FULL HIERARCHICAL PATH.

Args:
    params (PatchContentInput): Contains:
        - filepath (str): Path to file
        - target_type (TargetType): 'heading', 'block', or 'frontmatter'
        - target (str): See examples below for correct format
        - operation (PatchOperation): 'append', 'prepend', or 'replace'
        - content (str): Content to insert

Returns:
    str: Success message with patch details

HEADING PATH EXAMPLES (MUST use full path with '/'):
    ✅ CORRECT:
       - target="Introduction" (for top-level # Introduction)
       - target="Methods/Data Collection" (for ## Data Collection under # Methods)
       - target="Results/Analysis/Statistical Tests" (for ### Statistical Tests under ## Analysis under # Results)

    ❌ WRONG:
       - target="Data Collection" (missing parent "Methods")
       - target="Statistical Tests" (missing parents "Results/Analysis")

BLOCK REFERENCE EXAMPLE:
    - target_type="block", target="^unique-block-id"

FRONTMATTER EXAMPLE:
    - target_type="frontmatter", target="tags"

Note: Always read the file first to see the exact heading structure before patching.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
paramsYes

Implementation Reference

  • Main handler function that implements the obsidian_patch_content tool logic. Handles patching content at headings, blocks, or frontmatter using read/modify/write pattern with helper functions.
    async def patch_content(params: PatchContentInput) -> str:
        """Insert content at specific locations within notes using headings, blocks, or frontmatter.
    
        CRITICAL: For heading targets, you MUST provide the FULL HIERARCHICAL PATH.
    
        Args:
            params (PatchContentInput): Contains:
                - filepath (str): Path to file
                - target_type (TargetType): 'heading', 'block', or 'frontmatter'
                - target (str): See examples below for correct format
                - operation (PatchOperation): 'append', 'prepend', or 'replace'
                - content (str): Content to insert
    
        Returns:
            str: Success message with patch details
    
        HEADING PATH EXAMPLES (MUST use full path with '/'):
            ✅ CORRECT:
               - target="Introduction" (for top-level # Introduction)
               - target="Methods/Data Collection" (for ## Data Collection under # Methods)
               - target="Results/Analysis/Statistical Tests" (for ### Statistical Tests under ## Analysis under # Results)
    
            ❌ WRONG:
               - target="Data Collection" (missing parent "Methods")
               - target="Statistical Tests" (missing parents "Results/Analysis")
    
        BLOCK REFERENCE EXAMPLE:
            - target_type="block", target="^unique-block-id"
    
        FRONTMATTER EXAMPLE:
            - target_type="frontmatter", target="tags"
    
        Note: Always read the file first to see the exact heading structure before patching.
        """
        try:
            # Read current content
            current_content = await obsidian_client.read_file(params.filepath)
            
            if params.target_type == TargetType.FRONTMATTER:
                # Update frontmatter field
                fm, body = obsidian_client.parse_frontmatter(current_content)
                
                if params.operation == PatchOperation.REPLACE:
                    fm[params.target] = params.content
                elif params.operation == PatchOperation.APPEND:
                    current_value = fm.get(params.target, "")
                    fm[params.target] = str(current_value) + "\n" + params.content if current_value else params.content
                elif params.operation == PatchOperation.PREPEND:
                    current_value = fm.get(params.target, "")
                    fm[params.target] = params.content + "\n" + str(current_value) if current_value else params.content
                
                new_content = obsidian_client.serialize_with_frontmatter(fm, body)
            
            elif params.target_type == TargetType.HEADING:
                # Find heading position
                position = find_heading_position(current_content, params.target)
                
                if position is None:
                    return json.dumps({
                        "error": f"Heading not found: '{params.target}'",
                        "filepath": params.filepath,
                        "hint": "Use FULL hierarchical path like 'Parent/Child/Grandchild'. Read the file first to see exact heading structure.",
                        "suggestion": "Use obsidian_get_file_contents to view the note's heading structure, then use the complete path.",
                        "success": False
                    }, indent=2)
                
                if params.operation == PatchOperation.APPEND:
                    new_content = current_content[:position] + "\n" + params.content + current_content[position:]
                elif params.operation == PatchOperation.PREPEND:
                    # Find start of heading line
                    lines_before = current_content[:position].split("\n")
                    heading_line_start = position - len(lines_before[-1]) - 1
                    new_content = current_content[:heading_line_start] + params.content + "\n" + current_content[heading_line_start:]
                else:  # REPLACE
                    # Find next heading or end of file
                    lines = current_content[position:].split("\n")
                    next_heading_idx = None
                    for i, line in enumerate(lines):
                        if re.match(r'^#{1,6}\s+', line.strip()):
                            next_heading_idx = i
                            break
                    
                    if next_heading_idx:
                        section_end = position + sum(len(l) + 1 for l in lines[:next_heading_idx])
                        new_content = current_content[:position] + "\n" + params.content + "\n" + current_content[section_end:]
                    else:
                        new_content = current_content[:position] + "\n" + params.content
            
            elif params.target_type == TargetType.BLOCK:
                # Find block reference
                position = find_block_position(current_content, params.target)
                
                if position is None:
                    return json.dumps({
                        "error": f"Block reference not found: {params.target}",
                        "filepath": params.filepath,
                        "success": False
                    }, indent=2)
                
                if params.operation == PatchOperation.APPEND:
                    new_content = current_content[:position] + "\n" + params.content + current_content[position:]
                elif params.operation == PatchOperation.PREPEND:
                    # Find start of block line
                    lines_before = current_content[:position].split("\n")
                    block_line_start = position - len(lines_before[-1]) - 1
                    new_content = current_content[:block_line_start] + params.content + "\n" + current_content[block_line_start:]
                else:  # REPLACE - replace the entire line containing the block reference
                    lines_before = current_content[:position].split("\n")
                    block_line_start = position - len(lines_before[-1]) - 1
                    line_end = current_content.find("\n", position)
                    if line_end == -1:
                        line_end = len(current_content)
                    new_content = current_content[:block_line_start] + params.content + current_content[line_end:]
            
            # Write updated content
            await obsidian_client.write_file(params.filepath, new_content)
            
            return json.dumps({
                "success": True,
                "message": f"Content patched successfully using {params.operation.value} on {params.target_type.value}",
                "filepath": params.filepath,
                "target": params.target
            }, indent=2)
            
        except ObsidianAPIError as e:
            return json.dumps({
                "error": str(e),
                "filepath": params.filepath,
                "success": False
            }, indent=2)
  • MCP tool registration decorator that registers the patch_content function as 'obsidian_patch_content' tool.
    @mcp.tool(
        name="obsidian_patch_content",
        annotations={
            "title": "Patch Content at Specific Location",
            "readOnlyHint": False,
            "destructiveHint": False,
            "idempotentHint": False,
            "openWorldHint": False
        }
    )
  • Pydantic input schema defining parameters for the tool: filepath, target_type (heading/block/frontmatter), target, operation (append/prepend/replace), content.
    class PatchContentInput(BaseModel):
        """Input for patching content relative to headings/blocks/frontmatter."""
        model_config = ConfigDict(str_strip_whitespace=True, extra='forbid')
        
        filepath: str = Field(
            description="Path to the file to patch",
            min_length=1,
            max_length=500
        )
        target_type: TargetType = Field(
            description="Type of target: 'heading' for markdown headers, 'block' for block references, 'frontmatter' for YAML metadata"
        )
        target: str = Field(
            description=(
                "IMPORTANT: For 'heading' type, MUST use FULL HIERARCHICAL PATH with '/' separator. "
                "Examples: 'Introduction' (top-level), 'Methods/Data Collection' (nested), 'Results/Analysis/Statistics' (deeply nested). "
                "For 'block' type: block reference like '^block-id'. "
                "For 'frontmatter' type: field name like 'tags' or 'status'."
            ),
            min_length=1,
            max_length=200
        )
        operation: PatchOperation = Field(
            description="Operation: 'append' to add after target, 'prepend' to add before target, 'replace' to overwrite target"
        )
        content: str = Field(
            description="Content to insert",
            min_length=1,
            max_length=50000
        )
  • Helper function to locate the exact position after a hierarchical heading path in markdown content (e.g., 'Parent/Child'). Used for targeted patching.
    def find_heading_position(content: str, heading_path: str) -> Optional[int]:
        """
        Find the position of a heading in markdown content.
        
        Args:
            content: The markdown content
            heading_path: Path like "Section/Subsection"
            
        Returns:
            Position after the heading line, or None if not found
        """
        parts = heading_path.split("/")
        lines = content.split("\n")
        
        current_level = 0
        current_path = []
        
        for i, line in enumerate(lines):
            # Check if this is a heading
            heading_match = re.match(r'^(#{1,6})\s+(.+)$', line.strip())
            if heading_match:
                level = len(heading_match.group(1))
                title = heading_match.group(2).strip()
                
                # Adjust path based on heading level
                if level <= current_level:
                    current_path = current_path[:level-1]
                
                current_path.append(title)
                current_level = level
                
                # Check if we found our target
                if current_path == parts:
                    # Return position at end of this line
                    return sum(len(l) + 1 for l in lines[:i+1])
        
        return None
  • Helper function to locate a block reference (e.g., '^block-id') in content for precise patching operations.
    def find_block_position(content: str, block_ref: str) -> Optional[int]:
        """
        Find the position of a block reference.
        
        Args:
            content: The markdown content
            block_ref: Block reference like "^block-id"
            
        Returns:
            Position after the block, or None if not found
        """
        # Remove ^ prefix if present
        block_id = block_ref.lstrip("^")
        
        # Look for the block reference
        pattern = rf'\^{re.escape(block_id)}\b'
        match = re.search(pattern, content)
        
        if match:
            return match.end()
        
        return None

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/Shepherd-Creative/obsidian-mcp'

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